Cyril Mottier

“It’s the little details that are vital. Little things make big things happen.” – John Wooden

ActionBar on the Move

Edit (11/27/12): Added a video demonstrating the result on a Nexus 7 running on Android 4.1

Over the past year, the ActionBar paradigm has become an essential component in the process of designing and developing an Android application. Indeed, the ActionBar has many advantages that help developers in future-proofing their apps. It contains contextual actions, can be customized fairly easily, is highly scalable, etc. Because of this, one should ALWAYS consider using the ActionBar UI pattern in one’s design process when creating a new Android app.

ActionBar features a lot of interesting styling APIs. These APIs let you brand your application so that it fits your design, while still being recognizable among other applications. Put simply, there are almost no limits to what you can do with an ActionBar. Until you try doing something more advanced …

Back in March 2012, I was in the process of designing AVélov, I really wanted to have an ActionBar that differentiated it from the other apps. So I came up with the idea of having an animated ActionBar background. AVélov being about bikes, I logically wanted the animation to be in relation with bikes (a bike riding from the left edge of the screen to the right screen, a spinning wheel, etc.).

In order to make sure this was possible I created a tiny app with an ActionBar. I rapidly built an AnimationDrawable, started it with a simple call to the start() method and used it as the ActionBar’s background. The result was pretty disappointing because it wasn’t animating at all. Exploring ActionBarContainer (a non-public View backing the ActionBar) source code I noticed it wasn’t registering a callback1 to my Drawable:

1
2
3
4
public void setPrimaryBackground(Drawable bg) {
    mBackground = bg;
    invalidate();
}

As a result, the Drawable had no way to notify the enclosing View to redraw itself at fixed time intervals. From my point of view this was a wanted behavior to avoid these web-of-the-90’s-red-to-yellow-blinking ActionBars. I finally decided to postpone the animation to a future release.

Recently I came back to this feature/enhancement and started developing a new Animatable Drawable for testing purposes. This very basic Drawable changes its color and animates the changes in a smooth fashion:

ColorAnimationDrawable.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class ColorAnimationDrawable extends Drawable implements Animatable {

    private static final long FRAME_DURATION = 1000 / 60;
    private static final long ANIMATION_DURATION = 1500;

    private static final int ACCCENT_COLOR = 0x33FFFFFF;
    private static final int DIM_COLOR = 0x33000000;

    private static final Random mRandom = new Random();

    private final Paint mPaint = new Paint();

    private boolean mIsRunning;

    private int mStartColor;
    private int mEndColor;
    private int mCurrentColor;

    private long mStartTime;

    @Override
    public void draw(Canvas canvas) {
        final Rect bounds = getBounds();

        mPaint.setColor(mCurrentColor);
        canvas.drawRect(bounds, mPaint);

        mPaint.setColor(ACCCENT_COLOR);
        canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.top + 1, mPaint);

        mPaint.setColor(DIM_COLOR);
        canvas.drawRect(bounds.left, bounds.bottom - 2, bounds.right, bounds.bottom, mPaint);
    }

    @Override
    public void setAlpha(int alpha) {
        oops("setAlpha(int)");
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        oops("setColorFilter(ColorFilter)");
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }

    @Override
    public void start() {
        if (!isRunning()) {
            mIsRunning = true;

            mStartTime = AnimationUtils.currentAnimationTimeMillis();
            mStartColor = randomColor();
            mEndColor = randomColor();

            scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);
            invalidateSelf();
        }
    }

    @Override
    public void stop() {
        if (isRunning()) {
            unscheduleSelf(mUpdater);
            mIsRunning = false;
        }
    }

    @Override
    public boolean isRunning() {
        return mIsRunning;
    }

    private void oops(String message) {
        throw new UnsupportedOperationException("ColorAnimationDrawable doesn't support " + message);
    }

    private static int randomColor() {
        return mRandom.nextInt() & 0x00FFFFFF;
    }

    private static int evaluate(float fraction, int startValue, int endValue) {
        return (int) (startValue + fraction * (endValue - startValue));
    }

    private final Runnable mUpdater = new Runnable() {
        @Override
        public void run() {
            long now = AnimationUtils.currentAnimationTimeMillis();
            long duration = now - mStartTime;
            if (duration >= ANIMATION_DURATION) {
                mStartColor = mEndColor;
                mEndColor = randomColor();
                mStartTime = now;
                mCurrentColor = mStartColor;
            } else {
                float fraction = duration / (float) ANIMATION_DURATION;
                //@formatter:off
                mCurrentColor = Color.rgb(
                        evaluate(fraction, Color.red(mStartColor), Color.red(mEndColor)),     // red
                        evaluate(fraction, Color.green(mStartColor), Color.green(mEndColor)), // green
                        evaluate(fraction, Color.blue(mStartColor), Color.blue(mEndColor)));  // blue
                //@formatter:on
            }
            scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);
            invalidateSelf();
        }
    };
}

I think the only interesting thing in this code is the method used to animate a color change. It consists of extracting each color component and animating these values and not the entire color.

I applied this Drawable to my ActionBar and boooom it was working! I was quite surprised and starting to investigate. After looking at the AOSP source code for the Jelly Bean MR1 release, I noticed the issue had been fixed by Adam Powell (an engineer at Google working on the UI toolkit) with a7cc06d. The code is now as described below:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setPrimaryBackground(Drawable bg) {
    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }
    mBackground = bg;
    if (bg != null) {
        bg.setCallback(this);
    }
    setWillNotDraw(mIsSplit ? mSplitBackground == null :
            mBackground == null && mStackedBackground == null);
    invalidate();
}

The problem with this fix is it wasn’t available for pre-API 17 builds. So I came up with a pretty simple solution for pre-API 17: registering a custom Drawable.Callback and invalidating the ActionBarContainer repeatedly setting the same Drawable with the ActionBar’s setBackgroundDrawable(Drawable) method:

MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class MainActivity extends Activity {

    private final Handler mHandler = new Handler();
    private ColorAnimationDrawable mActionBarBackground;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        mActionBarBackground = new ColorAnimationDrawable();
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            mActionBarBackground.setCallback(mDrawableCallback);
        } else {
            getActionBar().setBackgroundDrawable(mActionBarBackground);
        }
        mActionBarBackground.start();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mActionBarBackground.start();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mActionBarBackground.stop();
    }

    private Drawable.Callback mDrawableCallback = new Drawable.Callback() {
        @Override
        public void invalidateDrawable(Drawable who) {
            getActionBar().setBackgroundDrawable(who);
        }

        @Override
        public void scheduleDrawable(Drawable who, Runnable what, long when) {
            mHandler.postAtTime(what, when);
        }

        @Override
        public void unscheduleDrawable(Drawable who, Runnable what) {
            mHandler.removeCallbacks(what);
        }
    };

}

Thanks to this trick, you can now animate your ActionBar’s background back to API 11 but please keep in mind this may have several and sometimes serious consequences on your application:

  • It can make your application look different and more polished by featuring tiny, subtle and nice details

  • When setting an animated background to an ActionBar, always make sure it is as subtle as possible. Animations should not distract or interrupt the user in his/her interactions with your app. For instance you could run the animation only when the user is not touching your Activity.

  • Using the technique described in this article forces the system to invalidate the whole ActionBarContainer for each animation frame. Reduce the duration of your animation as much as possible as it can be CPU & GPU consuming

  • The animated background Drawable should not be something essential to your app. Drawable should only be considered as styling component and not interaction components.


  1. I could write an entire book chapter about the Drawable notion. Put simple, when setting a Drawable as a View background, the View registers itself as the Drawable’s callback. This let the Drawable invalidate the View it is attached to. In other words, it lets you create Drawables that can refresh/redraw themselves. Android experts will also say it lets you easily leak Contexts when keeping a static reference to a Drawable.