Cyril Mottier

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

Pushing the ActionBar to the Next Level

Back in November 2012, I wrote a blog post entitled “ActionBar on the Move”. This article was mainly dealing with a technique to nicely and uniquely animate your ActionBar. Although I mentioned some of the effect’s possible applications, I never had time to effectively add an ActionBar animation to one of my own apps nor saw an application on the Play Store taking advantage of it.

While being at Google I/O last week, I finally found an application using the ActionBar animation technique. Let’s be honest, it literally blew my mind the first time I saw it. I felt in love with the nice, subtle and yet extremely useful animated effect probably more than the entire app itself! I am pretty sure you know the application I am talking about as it has been presented during the Google I/O keynote. You have also probably recently received an update of it: Play Music!

The latest update of Play Music (v5.0) has been completely redesign and features a brand new artist/album detail screen. If you open such a detail screen, you’ll notice the ActionBar is initially invisible and overlaps a large image describing the artist/album. Once you start scrolling down (if possible), the ActionBar fades in gradually. The ActionBar turns completely opaque when the large image has been scrolled out of the screen.

Here are two main advantages of this ActionBar animation:

  • Polish the UI: animations synchronized on an element you’re interacting with are generally appreciated by users because it makes them feel the UI is natural and reacts to their actions. The fading animation is a direct consequence of the per-pixel scrolling state and not a launched-once animation.

  • Take advantage of the screen real estate: while still preserving the UX of the platform, this pattern let the user primarily focus on the content rather than the controls. Used in addition to a nicely designed screen, it can be a game changer for your app’s interface.

In this article, I will deep dive into the details of implementing the technique described in “ActionBar on the Move” to create an effect similar to the one used in the Play Music app.

In order to better understand the goal we are targeting, you can have a look at the screenshots below or alternatively download the sample application.

Application theming/styling

As you can easily notice, in order to reproduce such an effect, the ActionBar must overlap the content of the screen. This can be easily done using the android:windowActionBarOverlay XML attributes. The code below describes the definition of the themes we’ll use:

values/themes.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme.TranslucentActionBar" parent="@android:style/Theme.Holo.Light.DarkActionBar">
        <item name="android:actionBarStyle">@style/Widget.ActionBar</item>
    </style>

    <style name="Theme.TranslucentActionBar.ActionBar" />

    <style name="Theme.TranslucentActionBar.ActionBar.Overlay">
        <item name="android:actionBarStyle">@style/Widget.ActionBar.Transparent</item>
        <item name="android:windowActionBarOverlay">true</item>
    </style>
</resources>

Pretty logically, the style of the ActionBar is defined in values/styles.xml as follows:

values/styles.xml
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Widget.ActionBar" parent="@android:style/Widget.Holo.Light.ActionBar.Solid.Inverse">
        <item name="android:background">@drawable/ab_background</item>
    </style>

    <style name="Widget.ActionBar.Transparent">
        <item name="android:background">@android:color/transparent</item>
    </style>
</resources>

Finally, we can use these themes in order to style our Activity.

AndroidManifest.xml
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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.cyrilmottier.android.translucentactionbar"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.TranslucentActionBar">

        <activity
                android:name=".HomeActivity"
                android:theme="@style/Theme.TranslucentActionBar.ActionBar.Overlay">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

Note that by using themes/styles we remove all potential flickering issues at startup (see Android App Launching Made Gorgeous for more information).

Getting the content ready

As explained previously, the ActionBar fading is synchronized on the per-pixel state scrolling of the scrolling container. In this example, we’ll simply use a ScrollView as a scrolling container. One of the major drawback of this container is you can’t register a listener in order to be notified when the scroll has changed. This can be easily done be creating a NotifyingScrollView extending the original ScrollView:

NotifyingScrollView.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
package com.cyrilmottier.android.translucentactionbar;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ScrollView;

/**
 * @author Cyril Mottier
 */
public class NotifyingScrollView extends ScrollView {

    /**
     * @author Cyril Mottier
     */
    public interface OnScrollChangedListener {
        void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt);
    }

    private OnScrollChangedListener mOnScrollChangedListener;

    public NotifyingScrollView(Context context) {
        super(context);
    }

    public NotifyingScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NotifyingScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mOnScrollChangedListener != null) {
            mOnScrollChangedListener.onScrollChanged(this, l, t, oldl, oldt);
        }
    }

    public void setOnScrollChangedListener(OnScrollChangedListener listener) {
        mOnScrollChangedListener = listener;
    }

}

Then, we can use this new scrolling container in an XML layout:

layout/activity_home.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<com.cyrilmottier.android.translucentactionbar.NotifyingScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/image_header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            android:src="@drawable/daft_punk"/>

      <! -- Some long content -->

    </LinearLayout>

</com.cyrilmottier.android.translucentactionbar.NotifyingScrollView>

Fading in/out the ActionBar

Now most of the boilerplate is ready, we can plug all of these components together. The ActionBar algorithm is rather simple and only consists on computing the alpha depending on the current per-pixel scrolling state of the NotifyingScrollView. Note that the effective scrolled distance must be clamped to [0, image_height - actionbar_height] in order to avoid weird values that may occur mainly because of the default over scroll behavior of scrolling containers on Android:

HomeActivity.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
package com.cyrilmottier.android.translucentactionbar;

import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.util.Log;
import android.view.Menu;
import android.widget.ScrollView;

public class HomeActivity extends Activity {

    private Drawable mActionBarBackgroundDrawable;

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

        mActionBarBackgroundDrawable = getResources().getDrawable(R.drawable.ab_background);
        mActionBarBackgroundDrawable.setAlpha(0);

        getActionBar().setBackgroundDrawable(mActionBarBackgroundDrawable);

        ((NotifyingScrollView) findViewById(R.id.scroll_view)).setOnScrollChangedListener(mOnScrollChangedListener);
    }

    private NotifyingScrollView.OnScrollChangedListener mOnScrollChangedListener = new NotifyingScrollView.OnScrollChangedListener() {
        public void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt) {
            final int headerHeight = findViewById(R.id.image_header).getHeight() - getActionBar().getHeight();
            final float ratio = (float) Math.min(Math.max(t, 0), headerHeight) / headerHeight;
            final int newAlpha = (int) (ratio * 255);
            mActionBarBackgroundDrawable.setAlpha(newAlpha);
        }
    };
}

As described in “ActionBar on the Move”, this snippet of code above doesn’t work for pre-JELLY_BEAN_MR1 devices. Indeed, the ActionBar isn’t invalidating itself when required because it isn’t registering itself as the Drawable’s callback. You can workaround this issue simply be attaching the following Callback in the onCreate(Bundle) method:

HomeActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) {
    }

    @Override
    public void unscheduleDrawable(Drawable who, Runnable what) {
    }
};
HomeActivity.java
1
2
3
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
    mActionBarBackgroundDrawable.setCallback(mDrawableCallback);
}

You can already run the code “as it”. Although the result looks alike the animation used in Play Music we can still continue to tweak it to make it better.

A final brush stroke

Enforcing ActionBar contrast

Having an transparent ActionBar may lead to design issues because you generally don’t know about the background you’ll be displayed on top of. For instance you may end up with a transparent ActionBar displaying a white text on top of a white description image. No need to say it makes the ActionBar invisible and useless.

The easiest way to avoid such a problem consists on modifying the image to make it a little bit darker at the top. Thus, in a worse case scenario (i.e. white image) we would have a grey area on top of the image making the ActionBar content (title, icons, buttons, etc.) visible.

A simple way to do that is to overlay a translucent dark to transparent gradient on top of the image. This can be done in XML only with the Drawable described below:

drawable/gradient.xml
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <size android:height="100dp"/>

    <gradient
        android:angle="270"
        android:startColor="#8000"
        android:endColor="#0000"/>

</shape>

The gradient is overlaid using a wrapping FrameLayout:

layout/activity_home.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/image_header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:src="@drawable/daft_punk"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/gradient"/>

</FrameLayout>

Avoid over-scroll

In Gingerbread (API 9), Android introduced a brand new way to notify the user a scrollable container is being scrolled beyond the content bounds. First it introduced the notion of EdgeEffect (available in the API starting API 14) and enabled over-scroll. While this is not a problem in general, it can be pretty annoying when one of the edge of your scrollable content is different from the background color.

You can reproduce it be simply flinging the ScrollView rapidly to the top and you’ll notice some white color (the background color) appears on top of the screen because the image is scrolling beyond the bounds. I personally consider this a a UI glitch and usually prefer disabling it in this rare cases.

One could imagine the best way to avoid over-scroll is to use View#setOverScrollMode(int) to change the mode to View#OVER_SCROLL_NEVER. Although it works, it also remove the edge effect which can be visually disturbing1. A simple way to do that is to modify the NotifyingScrollView to force the maximum over scroll values to zero when necessary:

NotifyingScrollView.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
private boolean mIsOverScrollEnabled = true;

public void setOverScrollEnabled(boolean enabled) {
    mIsOverScrollEnabled = enabled;
}

public boolean isOverScrollEnabled() {
    return mIsOverScrollEnabled;
}

@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY,
                               int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
    return super.overScrollBy(
            deltaX,
            deltaY,
            scrollX,
            scrollY,
            scrollRangeX,
            scrollRangeY,
            mIsOverScrollEnabled ? maxOverScrollX : 0,
            mIsOverScrollEnabled ? maxOverScrollY : 0,
            isTouchEvent);
}

Conclusion

I seriously don’t know if the team behind the Play Music application decided to implement the behavior based on my article. But it appears they brilliantly used the technique to both polish and emphasize the UI. It is clearly an awesome pattern to use whenever you need to design a screen which content is self-explanatory and is more important than the ActionBar content itself.


  1. Do not ask me why the naming of the constants/method is so ambiguous…