Tuesday, April 2, 2013

Automatically Managing Bitmaps from URLs via UrlBitmapDrawable

If you develop apps, you'll probably (at some point) need to load images from the internet.  Sometimes you'll load many images at once, as in a ListView or Gallery.  Your first foray will be easy, using BitmapFactory to decode a stream - then you run into the dreaded OutOfMemory error and are suddenly grounded back to the reality that is limited memory space.

There are two equally important problems to handle when using images: loading an image to view it, and unloading it later to save memory.  Last time I looked there were plenty of libraries out there for solving the first problem, but I was unsatisfied with their solutions to the second.  A lot of them simply blow away loaded Bitmaps at some point in time - but what if you still wanted to use it?  Then you end up in this complex tightrope walk, where you're having to constantly check and re-check your Bitmaps for whether they still exist.  It's a gigantic pain and I've been doing this dance forever.

I set out to solve the problem in a better way, such that it would do three things:

1. Load images from a URL into a View.

2. Unload that image later (for memory's sake).

3. Re-load that URL into the View if we ever see it again.

I was able to accomplish all three with the help of a class I wrote, UrlBitmapDrawable.

Introducing UrlBitmapDrawable


You can check out the code here: https://gist.github.com/dlew/69e6557604926d7e1513

You can use it just about anywhere as a Drawable.  Just instantiate it then use it:

UrlBitmapDrawable drawable = new UrlBitmapDrawable(res, myUrl, myFallback);
myView.setBackgroundDrawable(drawable);

With ImageViews there's a bit of a hack I needed to do (in order not to have to use a custom ImageView).  So I setup another method for using UrlBitmapDrawable with ImageViews:

UrlBitmapDrawable.loadImageView(myImageUrl, myImageView, myFallback);

Highlights


This solution has made my life easier in four ways:

It's Simple - All you need to is provide it a URL and it takes care of the rest.  You don't ever have to worry about its state; if the underlying Bitmap is recycled, it will fallback to the default image and start reloading the URL.

It's a Drawable - By making it a Drawable, it meant I could attach it to any View.  Tying it to a custom View would have vastly limited its potential.

It's Compatible - It's not tied to any particular implementation for loading images.  Retrieving images could be a simple network call, or you could hook it up to a complex LRU memory/disk cache.

It's Extensible - The version I've provided is simple; internally we've added some bells and whistles to it.  See "Areas for Improvement" below.

Disadvantages


It's not all sunshine and daisies.  There are two problems with UrlBitmapDrawable; however, I considered them minor in comparison to the larger problem I was trying to solve.

BitmapDrawable - BitmapDrawable does not let you access the underlying Bitmap by default, so you'll have to import your own version that opens it up.  Here's the source code from github.

ImageView Hack - In order to get ImageView to re-measure the Drawable's width/height after loading you have to trick it into thinking the Drawable has changed (by nulling it then resetting it back).  To be honest, there might be a better solution here, but I haven't found it.

Areas for Improvement


Here's ways that we've tricked out our UrlBitmapDrawable:

Default Resources via Id - A default resource is important (so we can show the user something before we load the asset).  In the sample code the UrlBitmapDrawable holds a Bitmap; this is a fine example, but if you're inflating a new default Bitmap per UrlBitmapDrawable, that can wreak its own memory havoc.

Internally, we've gone a more complex route which uses Resources and resIds, and loads the Bitmap automatically from an in-memory cache.  It keeps us from spending a lot of time (and memory) reloading the same default bitmaps.

Fallback URLs - We are sometimes given a list of URLs at differing quality levels.  It's pretty easy to hook that into this system; each time a download fails, try the next URL.

Recycling Bitmaps - If you read the code carefully you may notice that I never actually dealt with the second step - unloading Bitmaps from memory.  We use an LruCache to handle this; as a result of UrlBitmapDrawable it can recycle Bitmaps with impunity.  It also means you can evict the entire cache at any time if you need the memory.

Wednesday, March 20, 2013

Is Your AsyncTask Running?

A short note about AsyncTask: Prior to 4.0 (and maybe prior to 3.0, but I haven't tested), AsyncTask.getStatus() might lie to you. If the AsyncTask is canceled, it won't set the status correctly; instead, it will remain RUNNING far after AsyncTask.onCancelled() finishes.

My initial thought was to use AsyncTask.isCancelled(), but you can run into some concurrency issues there if you're trying to gauge whether the AsyncTask is done from another thread.  A cancelled AsyncTask doesn't necessarily end the moment you cancel it; in fact, if you're not checking isCancelled() regularly in doInBackground() then you can end up having the AsyncTask run for a while after you cancel.

My solution is to set a boolean at the end of onCancelled() that will indicate to the system that you got to the end of execution.  Here's an example of writing an AsyncTask where you can properly know when it's been finished:

private class MyAsyncTask extends AsyncTask {
  private boolean mFinishedCancel = false;

  protected Void doInBackground(Void... params) {
    return null; // You'd normally do something here
  }

  protected void onCancelled() {
    mFinishedCancel = true;
  }

  public boolean isFinished() {
    return getStatus() == Status.FINISHED || mFinishedCancel;
  }
}

Tuesday, March 12, 2013

Easier View State Saving

If you write your own custom Views you may eventually want to leverage onSaveInstanceState() and onRestoreInstanceState() to store some data relating to the View.  Unlike Activity/Fragment, which use Bundle to pass around the instance state, Views use Parcelables for their instance state.

Parcelable is fast, efficient, and kind of a pain in the ass to setup.  It's doubly painful for custom Views as you have to ensure that you're properly saving the superclass' state as well in your own Parcelable.  The boilerplate code is excessive.

It turns out there's a much easier way which leverages these two points:

  1. Bundles implement Parcelable.  This means you can use a Bundle instead of a custom Parcelable as your instance state.
  2. Bundles can store Parcelables.  This means you can preserve the superclass' state in your Bundle.

Here's a simple implementation of the concept:
public class CustomView extends View {
  public CustomView(Context context) {
    super(context);
  }

  @Override
  protected Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    bundle.putParcelable("superState", super.onSaveInstanceState());
    // Put whatever you want into the Bundle here
    return bundle;
  }

  @Override
  protected void onRestoreInstanceState(Parcelable state) {
    Bundle bundle = (Bundle) state;
    super.onRestoreInstanceState(bundle.getParcelable("superState"));
    // Restore what you put into the bundle here
  }
}

At this point you can store any primitives in a Bundle as you normally would in an Activity/Fragment.  Plus, if you use this trick many times you could even wrap it in a subclass of View (and simply replace the default instance state with a Bundle version of it).

Tuesday, February 26, 2013

Resistance/Avalon App

I've once again taken a detour from my more serious Android development to do some silly side projects.

The first of them is an app for the board game The Resistance and The Resistance: Avalon.  I've recently become obsessed with this game because it's just a ton of fun.  The game involves a lot of hidden roles, so there's a lengthy boot-up sequence where each person's allegiances are determined.  When you use all the roles available, it gets to be a bit of chore.  So I've written a dumb app that uses Android's TTS to speak the setup out loud, making the process a tiny bit easier.

The application can be found here: https://play.google.com/store/apps/details?id=com.idunnolol.resistance

Open source code here: https://github.com/dlew/android-resistance

As is usually the case with side projects, the time I put into the project vastly outweighs the time I'll ever save by using the app.  As such, I took this as an opportunity to try out two things: Android's TextToSpeech capabilities, and Maven Android builds.

TextToSpeech

I found TextToSpeech to be far easier to use than I expected.  It took me almost no time to get it up and running.  The only snag I ran into was using the OnUtteranceCompletedListener.  You need to give an utterance id to something you play before the listener will fire:

HashMap<String, String> params = new HashMap<String, String>();
endParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "myUtteranceId");
mTTS.speak("Say Something", TextToSpeech.QUEUE_ADD, params);

Maven Android Builds

I've been woefully behind the times with regards to Android build technology.  For years I've seen open source github projects using Maven but I've always ignored it because I'm scared of the big angry pom.xml files.  So I determined that I would use this simple app to teach myself Maven (via maven-android-plugin).

I found the initial setup of Maven to be pretty simple.  I had the samples up and running in no time using the "getting started" section of the site.  I even got Eclipse building the application using Maven using m2e-android.  So far so good.

I ran into a brick wall when I tried to add a library (in particular, ActionBarSherlock).  The command line Maven worked just fine when I added the library dependency, but I happen to enjoy the amenities of a modern IDE so it must work in Eclipse.  But in Eclipse, it wouldn't build - it complained about a missing dependency.  It turns out that you need to still manually do stuff for each library anyways if you're using Eclipse + Maven (unless I'm mis-reading the state of apklib, which is entirely possible).  Wasn't that the whole reason I started to use Maven in the first place?  To simplify my build process?

I think I'll keep making pom.xml for command line building/testing, but for actual dev in Eclipse it actually sets me back to use Maven.  Perhaps it integrates better with IntelliJ?  That alone may be reason to switch.  But at this point I'm far more excited for the upcoming Gradle builds.

One More Thing

If there was one cool thing I did with the code, it was the setup of Config.java.  Originally I had it with a bunch of booleans, one for each option; but this led to a lot of switch-like code that just felt repetitive. By converting it to an enum keyed-boolean store, I was able to automate a lot of app.  I always love it when you can greatly simplify and condense the code at the same time.

Tuesday, November 20, 2012

Why Do Motorola's ListViews Look Different

There was a helpful article "Why Does My ListView Look Different?" that Motorola posted a few years ago.  Unfortunately it has disappeared from the internet, as you might tell by clicking on the link.  It had some crucial information on fixing a weirdness for Motorola devices that I wanted to preserve for posterity (in case anyone else starts looking for it).

Essentially, when you're on some Motorola devices, the bottom of the ListView has a shadow on it by default.  In addition to that, if the ListView is supposed to take up the entire screen (even if it's not full), it doesn't.  Here's a screenshot of the bug in action:


The problem is android:overScrollFooter.  Motorola has a default one set and it causes sadness.  To get rid of it, set android:overScrollFooter="@null" in your ListView.  For bonus points, set it in a style and set that as your theme's default ListView so you never have to deal with this problem again.

Monday, November 12, 2012

The Unknown Style: actionBarWidgetTheme

One of the cooler features I've found in the Android Action Bar is grouped menu items.  If you group together a series of menu items in a submenu, you get a pretty popup of those items.  For example, if you set a submenu with android:checkableBehavior="single" then it'll have a radio button next to each item in the popup menu:



I wanted to reskin the RadioButton so that they all looked Holo, even on versions using ActionBarSherlock.  Naturally, I turned to my theme and set the radioButtonStyle.  However, the code below did not work:
<style name="MyTheme" parent="@android:style/Theme.Holo.Light.DarkActionBar">
    <item name="android:radioButtonStyle">@style/MyRadioButtonStyle</item>
</style>

<style name="MyRadioButtonStyle" parent="@android:style/Widget.CompoundButton.RadioButton">
    <item name="android:button">@drawable/my_radio_button</item>
</style>

I was fairly lost for a few hours because radioButtonStyle was clearly working on everything except for the widget popup.  I could insert RadioButtons into my Activity and they'd pick up the new style, but the old style would remain for the action bar.  What was going on?

The culprit is android:actionBarWidgetTheme.

Introduced in API 14, what it does is let you style Views inflated by the action bar separately from the rest of your Application or Activity.  This is a neat trick, but if you don't know it exists, you could easily get lost on why your styles aren't applying to action bar Views.

If android:actionBarWidgetTheme is undefined, it falls back to the current theme's values.  For most Android themes, that means you're fine.  But in the case of Theme.Holo.Light.DarkActionBar, it does set the actionBarWidgetTheme to "@android:style/Theme.Holo".  As a result, it will not fallback to your theme's default value.

I solved the problem by setting my own actionBarWidgetTheme that is a sub-style of the original actionBarWidgetTheme:

<style name="MyTheme" parent="@android:style/Theme.Holo.Light.DarkActionBar">
    <item name="android:actionBarWidgetTheme">@style/MyActionBarWidgetTheme</item>
</style>

<style name="MyActionBarWidgetTheme" parent="@android:style/Theme.Holo">
    <item name="android:radioButtonStyle">@style/MyRadioButtonStyle</item>
</style>

It's important to properly parent the actionBarWidgetTheme with whatever the theme was previously setting as its parent; otherwise you may miss out on some styling elsewhere.

Friday, November 9, 2012

Styling an AutoCompleteTextView

I ran into some confusion recently styling some AutoCompleteTextViews.  An AutoCompleteTextView is a compound View - it's got both an EditText component and a floating dropdown component.  The former is rather straightforward to style, by the dropdown is difficult because it's a mixture of attributes on the AutoCompleteTextView itself and styles set in the theme via android:dropDownListViewStyle.

For example, if you want to setup a custom background on the dropdown, you do it in the AutoCompleteTextView:

<AutoCompleteTextView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:popupBackground="@drawable/bg_autocomplete" />

But if you want to change the dividers, you have to create a theme and point that to a style, which isn't an immediately obvious solution:

<style name="MyTheme">
  <item name="android:dropDownListViewStyle">@style/DropDownListViewStyle</item>
</style>

<style name="DropDownListViewStyle">
  <item name="android:divider">#4F4F4F</item>
  <item name="android:dividerHeight">1dp</item>
</style>

The worst is when attributes in the AutoCompleteTextView and normal ListView styles collide.  I made the mistake of assuming that since it adopts a ListView-like style, it would adopt all ListView attributes - like android:listSelector.  The below, however, does not work:

<style name="DropDownListViewStyle">
  <item name="android:listSelector">@drawable/list_selector</item>
</style>

Instead, AutoCompleteTextView has its own attribute, android:dropDownSelector.  You have to set it like this:

<AutoCompleteTextView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:dropDownSelector="@drawable/list_selector" />