Wednesday, October 23, 2013

DateUtils.getRelativeTimeSpanString() and Timezones

If you're thinking about DateUtils.getRelativeTimeSpanString(): don't.

After a long struggle I've finally given up on the method.  It seems so tempting - an easy way of showing the user how far away an event will be (or was).  But without being able to pass a timezone you run into an intractable situation which makes it unusable.

The critical problem relates to how it calculates a number of days.  It relies on Time.getJulianDay(), which would work - except you have no control over the timezone parameter.  Instead, it uses the default device timezone.  This can cause day calculations to be incorrect as you might shift a Julian day when applying the device timezone to the milliseconds provided from another timezone.

Before 4.3, the situation was even worse: the internal Time used in DateUtils (for calls to getJulianDay()) is cached.  That meant if your app uses getRelativeTimeSpanString(), then the device's timezone changes, you're now calculating the # of days based on the previous device timezone!

The only comprehensive solution I can come up with is to implement my own version of getRelativeTimeSpanString() that adds the timezone element.  You just need to account for timezones when you're dealing with any period of time greater than hours.

Thursday, October 3, 2013

Centering Single-Line Text in a Canvas

Suppose I want a custom View that draws a circle, then a number centered inside of it.  Your first attempt will probably look something like this:


The problem is that while you can easily set a horizontal alignment for your TextPaint (via Paint.Align), the vertical alignment is tricky.  That's because Canvas.drawText() starts drawing at the baseline of your set Y-coordinate, instead of the center.

If you only knew the height of the text, then you could center it yourself - but getting the height is tricky!  TextPaint.getTextBounds() doesn't work quite right because it gives you the minimal bounding rectangle, not the height that the TextPaint draws.  For example, if your text has no ascenders/descenders, then the measured height is smaller than it will draw (since it will still account for the possibility of them).

The way I've found to get the height of the TextPaint is to use ascent() and descent().  These measure the size above/below the text's baseline.  Combined, they add up to the total height of the drawn text.  You can then use some math to center the draw on the baseline - here's a version of onDraw() that does it correctly*:

protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);

  Paint paint = new Paint();
  paint.setColor(Color.BLACK);

  TextPaint textPaint = new TextPaint();
  textPaint.setColor(Color.WHITE);
  textPaint.setTextAlign(Paint.Align.CENTER);
  float textHeight = textPaint.descent() - textPaint.ascent();
  float textOffset = (textHeight / 2) - textPaint.descent();

  RectF bounds = new RectF(0, 0, getWidth(), getHeight());
  canvas.drawOval(bounds, paint);
  canvas.drawText("42", bounds.centerX(), bounds.centerY() + textOffset, textPaint);
}

And the finished product is here:


Note that all advice in this post is about a single line of text.  If you're handling multi-line text then the solution is more complex because you have to handle how many lines the text will render onto.  If I ever try to tackle that I'll write that up as well.

* In a real-world example, you wouldn't want to instantiate your Paints in onDraw(); this is done for brevity's sake.

Tuesday, September 17, 2013

Upcoming Conferences

I just wanted to let anyone interested know about some upcoming conferences I'll be speaking at...

Droidcon London
London, October 24-27th
Crafting Unique, Delightful Apps (9:45 AM, Thursday, October 24th)

I'll be giving this talk in conjunction with our awesome designer, Chris Arvin.  To be honest, 45 minutes is hardly enough time to talk about all the things we could, but I'll be around all week long to talk more.

I'm also planning on attending the hackathon that weekend.  I've never done a hackathon before so it should be fun.  My only concern is that I have zero interest in staying up all night coding.  I think 8 hours of sleep (and some mental rest) is far more valuable than being drained all Sunday.

AnDevCon
San Francisco, November 12-15th
I Can Animate and So Can You (8:30 AM, Friday, November 15th)

I've started embracing animations in the last year in a way I never had before.  I regarded them as chunky and painful, which was true back in the days of 1.x and 2.x, but that's no longer the case.  It's possible to make really awesome animations now but it's still difficult to fit them into your app's structure.  This talk is designed to get you to think about how to do advanced animations in your app.

I also have a discount code for AnDevCon.  It'll knock $200 off registration.  Just use the code "LEW".

Tuesday, September 3, 2013

Smoothing performance on Fragment transitions

Suppose you're doing a pretty standard Fragment replacement with a custom animation:

getSupportFragmentManager()
    .beginTransaction()
    .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
    .replace(android.R.id.content, new MyFragment())
    .commit();

You may notice that the performance can be a bit rough, not as smooth as you'd like. A common way to improve Android animation performance is to use hardware layers.  Normally you'd add it to the animation directly but with fragments you don't get access to it unless you take advantage of Fragment.onCreateAnimation()*.  Here's how it looks:

public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    Animation animation = super.onCreateAnimation(transit, enter, nextAnim);

    // HW layer support only exists on API 11+
    if (Build.VERSION.SDK_INT >= 11) {
        if (animation == null && nextAnim != 0) {
            animation = AnimationUtils.loadAnimation(getActivity(), nextAnim);
        }

        if (animation != null) {
            getView().setLayerType(View.LAYER_TYPE_HARDWARE, null);

            animation.setAnimationListener(new AnimationListener() {
                public void onAnimationEnd(Animation animation) {
                    getView().setLayerType(View.LAYER_TYPE_NONE, null);
                }

                // ...other AnimationListener methods go here...
            });
        }
    }

    return animation;
}

Now the animation should be a lot more smooth!  In my own code, I've overridden this method in a base Fragment from which all others extend so that I always get this feature (though if you're more particular you could only apply it to certain Fragments).

* If you're not using the support library, then you'll be overriding Fragment.onCreateAnimator() and using animator-based classes.

Tuesday, August 20, 2013

Joda Time's Memory Issue in Android

I've recently gotten fed up with how nuts the built-in calendar library is for Java/Android.  It's incredibly easy to make mistakes and it's unintuitive at best, so I've finally decided to take the plunge and switch to Joda time.

Joda is like a dream come true* except for one fairly extreme memory issue that I ran into.  After adding it to the app we started to see two huge memory sinks show up in MAT: a JarFile and a ZipFile that in our app took up a combined 4MB!



Joda's JAR is only half a meg, so how come these things took up so much space?  Why didn't any other JARs take up this space?  What's even stranger is that the amount of memory used seemed to scale based on the number of resources I had in the application; a simple test app only used up an extra 700kb, but Expedia's resource-heavy app took up the above.

It turns out the problem is ClassLoader.getResourceAsStream().  Joda time includes the olson timezone database in the JAR itself and loads the TZ data dynamically through getResourceAsStream().  For some reason getResourceAsStream() does some rather extreme caching and takes up a ton of memory if you use it**.

Thankfully there's a fairly simple solution.  You can actually implement any timezone Provider you want, circumventing the normal JAR-based ZoneInfoProvider.  Just make sure that your implementation has a default constructor and setup your system properties thus:

System.setProperty("org.joda.time.DateTimeZone.Provider",
    AssetZoneInfoProvider.class.getCanonicalName());

As such, I imported all of the TZ data (compiled, from the JAR) into my project's /assets/ directory.  Then I took the source for ZoneInfoProvider and reworked it so that openResource() uses the AssetManager to retrieve data.  I hooked it up and voila - no more excessive memory usage!  As an added bonus, this makes it a lot easier to update your TZ data without relying on a new version of Joda time.

As an epilogue, if someone can explain why getResourceAsStream() causes the sadness it does I'd be interested to know.  I tried looking into it for a bit but gave up because it wasn't like I would be able to change the system code anyways.

* Seriously: if you deal with dates, times, or some combination thereof at all, you will be doing yourself a favor by switching to Joda time.

** What initially tipped me off was a Jackson XML post about the same problem: https://github.com/FasterXML/jackson-core/pull/49

Wednesday, July 3, 2013

Don't Override ListView.getAdapter()

A few days we ran into a bug.  When we were updating the underlying data for the particular Adapter, the ListView would blow up with this exception:

E/AndroidRuntime(1809): java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(16908298, class com.expedia.bookings.widget.ItinListView) with Adapter(class android.widget.HeaderViewListAdapter)]

The thing is, we weren't updating from a background thread.  That exception is thrown when the # of items the ListView thinks the adapter has and the # it actually has are out of sync, but I couldn't see how that was happening.  What was going on?

It turned out to be me overriding ListView.getAdapter().  There's some code I've been working with recently which has a custom ListView; inside of it is a custom Adapter.  I wanted access to that Adapter (but not the ListAdapter wrappers that are sometimes added in the case of header/footer views), so I overrided ListView.getAdapter() and had it return the custom Adapter.

However, the ListView uses getAdapter() sometimes to get the count for the # of items it has.  By bypassing the wrapper ListAdapter, the count was sometimes wrong (since it wasn't accounting for header/footer Views).

The moral of the story is: don't override ListView.getAdapter().

Thursday, June 27, 2013

How to Correctly Format Date/Time Strings on Android

One aspect of internationalization is to correctly format your date/time strings.  Different countries use very different formats and it's easy to incorrectly format your strings for your international users.

Your first foray into formatting date/times is probably through java.text.DateFormat (via SimpleDateFormat):

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String date = df.format(cal.getTime());
// date == "2013-12-20"

While this works great for formatting parameters (for, say, a web service), it's terrible for localization.  Your international users won't be using the same date/time format you're using and it won't pick up user preferences (e.g., date order or 12-hour vs 24-hour).

A more correct way of doing it is to use android.text.format.DateFormat (not to be confused with the previous DateFormat).  There are some methods here that return formatters defined by the system's locale, like getDateFormat() and getTimeFormat() (among others):

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = android.text.format.DateFormat.getDateFormat(this); 
String date = df.format(cal.getTime());
// date == "12/20/2013"

The problem with these formatters is that they are inflexible; what if you don't want to show a year on a date?  What if you want to include the day of the week?  There are only limited circumstances where these formatters are good enough.

The best solution is to use DateUtils.  It has two powerful methods - formatDateTime() and formatDateRange() - which take in flags to determine which fields to include.  It automatically formats to the user's locale and preferences without you having to worry about it.

DateUtils.formatDateTime() formats a single point in time.  Here's a few examples:

Calendar cal = new GregorianCalendar(2013, 11, 20);
String date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR);
// date == "12/20/2013"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME);
// date == "00:00, 12/20/2013"

DateUtils.formatDateRange() formats a range, like "Jan 5 - Feb 12".  Why might you want to use this instead of just concatenating two calls to formatDateTime()?  Besides being easier, it can optimize output in certain circumstances by reducing redundant field usage, like months/years when they don't change throughout the range:

Calendar cal1 = new GregorianCalendar(2013, 11, 20);
Calendar cal2 = new GregorianCalendar(2013, 11, 25);
Calendar cal3 = new GregorianCalendar(2014, 0, 5);
String date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal2.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20 - 24"
date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal3.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20, 2013 - January 4, 2014"

One thing to watch out for with formatDateRange() is where it cuts off the day.  You may notice in the example above that the date ranges seem to be off by one day; that's because it cuts off at midnight.  If you add a millisecond it should properly format the range.

If you want your application to abide by the locale's formatting rules while still having control over what information to show, DateUtils is your place to go.  Be sure to read through all the different formatting flags so you can wield the most power with this tool.

(One final note - the example code above shows output in my locale.  In your locale it may differ - this is on purpose, of course!)