Saturday, February 23, 2013

Custom Paint Job: Accelerating the Ultimate Stopwatch

(Edit - 1st July 2013: Animator runnable updated again and one more layer of overdraw removed, details in new blog post)

Creating a custom Android view in 2013 isn't exactly breaking new ground, but the time was long overdue to relegate the old SurfaceView to history and move on; a task I was looking forward to but had kept putting off. Anyway, +Marie Schweiz's great new Ultimate Stopwatch designs deserved a hardware accelerated home.

The Android Developers training class breaks down the job of developing a Custom view into a few simple steps:
  1. Subclass a View
  2. Implement custom drawing
  3. Make the view interactive
In practice, when it came to the animations I also grabbed a couple of hints from Anders Ericsson's post at JayWay, as the mental model is also slightly different and I'd seen his related talk at Droidcon London 2012. In the SurfaceView I had a thread constantly looping to call the update/draw methods, however in a custom view it will redraw each time you call invalidate(). So, instead of a looping thread I can just call invalidate() after each cycle completes to start the next paint. This is done by posting a runnable to the View's message queue, I'm aiming for 15ms update cycles for just over 60fps.

    private Runnable animator = new Runnable() {
        public void run() {

            if (mIsRunning) postDelayed(this, 15);
The onDraw() methods from my SurfaceView and Custom view are identical, so just a copy and paste job there. The onTouch() and Activity communications handler also remained untouched. So in the end the Custom view is considerably simpler as there is no surface creation/destruction to worry about or separate thread to maintain.

A really nice feature of a Custom view is the ability to configure your own XML attributes. Previously I had to instantiate the SurfaceView and pass in details on whether it should represent the Stopwatch or Countdown view, with a Custom view I can add the following into attrs.xml

        <declare-styleable name="StopwatchCustomView">
            <attr name="watchType" format="enum">
                <enum name="stopwatch" value="1"/>
                <enum name="countdown" value="0"/>

Then by adding the name space xmlns:custom="" in my layout, I can use the attribute custom:watchType="stopwatch" in the layout file and retrieve it in the code during instantiation. Being able to configure the custom view in XML can really simplify code and make the source easier to understand. Here is the code to retrieve the value:

    TypedArray a = context.getTheme().obtainStyledAttributes(
         0, 0);
        isStopwatch = a.getBoolean(R.styleable.StopwatchCustomView_watchType,true);

That's it. The view is hardware accelerated where available and due to some optimizations the refresh rate has also increased when h/w acceleration is absent.

Some small improvements were gained by refactoring, but the main increases were delivered using the 'Show GPU overdraw' feature of Android's Developer Options, which is now available as I'm using the GPU to draw. It's immediately clear that before optimization there's excessive overdraw, highlighted by the red areas. This was almost entirely down to background colours being set on layers that weren't visible; the ViewPager, the layout of the custom view and also in the onDraw() of the Custom View. Eliminating these unseen, but still drawn, background colours reduced the graphics load and improved performance.

Now we have a wondrous, hardware accelerated Custom view we're in a much better position to add some final flourishes to improve the application. Later I'll write about some custom animations I've added, as suggested by +Nick Butcher to not only improve the perceived quality but also the usability.

The Ultimate Stopwatch and Timer is open source at, designed by +Marie Schweiz and makes use of the awesome ActionBarSherlock by Jake Wharton.

1 comment:

Mike said...

You might want to consider using View.postOnAnimation rather than manually timing your animation.