Monday, November 25, 2013

Sample Android Project: Movies

There's a sample application that I worked on recently with talented designer Chris Arvin for a talk we gave at DroidCon UK 2013.  I also repurposed part of it for the talk about animations I gave at AnDevCon a few weeks later.

The app is open source and you can check it out here: https://github.com/dlew/android-movies-demo

You can get the APK here: https://github.com/dlew/android-movies-demo/releases/



The interactions required a bit of interesting engineering so I wanted to discuss some of it here.

ViewPager and Decor

One of the key interactions we wanted was the app to feel like a ViewPager so that it was an interaction users were familiar with except that the result isn't quite the same - instead of paging, content would slide out from underneath other content.  This is not something ViewPager is inherently designed to do.

There were two options open to me.  One was to rip out all of ViewPager's event handling code and make my own custom View, which was not an appealing prospect.  The other was to find some way to manipulate ViewPager itself.

It turns out there is an interface ViewPager finds special called Decor.  If a View implements Decor, then ViewPager treats it as a special View that can remain on top of it the ViewPager.  This is normally for implementing your own tabs, but in this case I imported my own copy of ViewPager (since Decor is hidden normally) and made my entire UI a Decor View.

It's a neat trick that would work for any app that wants ViewPager event handling without paging Views, though I think if you wanted a less hacky solution you'd write your own event code.

Custom Views Everywhere

I've gone from avoiding custom Views like the plague to fully embracing them.

The key realization is that custom Views do not need to handle every situation.  If you're looking at framework custom Views as reference you will be overwhelmed quickly.  For example, during measurement framework Views have to handle WRAP_CONTENT, MATCH_PARENT, and everything in between; but if you know your View is always going to be MATCH_PARENT you can greatly simplify all your code.

The movies sample app is a pile of custom Views.  First there's SlidingRevealViewGroup, which generically shows one View slide out from another.  On top of that is built MovieRowView, which has the specific Views that we want to display.  Then there's SlidingPairView, which is a set of two SlidingRevealViewGroups that creates the side-by-side effect seen in the app.

I also needed a few other custom Views to shore up some other issues.  CenteringRelativeLayout just adjusts the film cover so that, as it shrinks in size, it still looks centered.  SlidingListView was required for performance; we needed to manipulate the rows directly when sliding, instead of constantly notifying of data changed.

Performance Tricks

The coolest part of the whole app is the slide: how the cover becomes smaller and the content slides out from underneath.  All of this was achieved through translation of Views.  I took advantage of the fact that ViewGroups don't render their children outside of their own bounds.  By translating content I could hide them outside the clip bounds.

When a slide starts, it throws practically everything into hardware layers then just slides Views left/right.  Moving around pre-rendered content is fast and as a result the paging is silky smooth.

Nowadays I'm a huge fan of those basic properties of Views (translation, scale, rotation and alpha).  They're the core reason to support ICS+ only; with these properties you can take your interactions to the next level.

Drawbacks

The drawback of the solution I came up with is that it creates a ton of overdraw.  This hurts performance, especially (as far as I can tell) when rendering a new row while scrolling up/down.  I sacrificed scrolling performance for paging performance.  Perhaps there is a way to achieve both, but I'm pretty much done with this sample for now.

The rounded corners could've been implemented in a much better fashion than an overlaid Drawable, but with the limited time before the presentation I had, I could not come up with a better solution.

Thursday, November 21, 2013

Is findViewById() Slow?

One of the presenters at Droidcon UK 2013 made the point that View.findViewById() has been unjustly villainized and that the ViewHolder pattern recommended by Google is unnecessary.  After hearing that I studied the code (in View and ViewGroup) and did not observe any heinous performance issues contained therein.  It's a pretty simple recursive lookup that walks the View hierarchy for an id.

As this article points out, findViewById() is necessarily slower than ViewHolder because it's O(n) vs. O(1).  But that doesn't mean findViewById() is slow.  If the operation is sufficiently fast and n is low then it doesn't matter if it's O(n).  Who cares if I have to do a 1 nanosecond operation a thousand times?

My hypothesis is that findViewById() is fast enough as to be negligible.  With that in mind, I cooked up a sample application that tests findViewById(): https://github.com/dlew/android-findviewbyid


You can dynamically create a hierarchy of Views, either adding to the depth or the number of children at each node.  When you hit "run" it searches for the furthest away id a number of times, then averages the time taken.  I ran the test on my Nexus 1, to somewhat recreate the situation described when Google explicitly recommended the ViewHolder pattern three years ago.

Here's the results from a test of many possible depth/views per node values: http://goo.gl/dK2vo3


As  can be seen from the chart, the time taken to use findViewById() is roughly linear with the number of Views in the hierarchy.  What's key here, though, is that for a low number of Views it barely takes any time at all - usually a matter of several microseconds.  And remember, this is on a rather old phone, the Nexus 1.

That said, if you were to call findViewById() many, many times it could cause a problem.  Admittedly this part of the post is going to be a bit more hand-wavy than the rest, but: I tried out scrolling on a ListView on my Nexus 1 - there is an upper bound to how fast I can scroll and I can't seem to break 60 getViews() per second.  That means that getView() is called once per 16ms frame.  Given that we're measuring findViewById() in microseconds, even calling it a dozen times (with a reasonable number of Views) shouldn't cause performance issues.

So my conclusion is that findViewById() is harmless in most practical circumstances.  I'll probably still use ViewHolder to avoid casting Views all the time, but performance will not be the main purpose anymore.

Disclaimer: I'm not a performance expert so I suspect there's something I'm missing (especially since I'm going directly against Google advice here).  If someone knows of a missing link let me know!

Monday, November 18, 2013

Android Talk Slides from 2013

I'm done with talk season!  In case you missed them, here's the slides:

Crafting Unique, Delightful Apps

I Can Animate and So Can You

I suggest downloading the Keynote files for the presentations.  They animate!

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.