Pushing the ActionBar to the Next Level

Android 专栏收录该内容
96 篇文章 0 订阅

原文地址:http://cyrilmottier.com/2013/05/24/pushing-the-actionbar-to-the-next-level/

sampleAPK下载地址:http://cyrilmottier.com/2013/05/24/pushing-the-actionbar-to-the-next-level/media/2013/05/pushing-the-actionbar-to-the-next-level/TranslucentActionBar.apk

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 NotifyingScrollViewextending 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 ActionBaralgorithm 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 theActionBar 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…
  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
<p style="color:#666666;"> <span style="font-size:14px;">本门课程重实战,将基础知识拆解到项目里,让你在项目情境里学知识。</span> </p> <p style="color:#666666;"> <span style="font-size:14px;">这样的学习方式能让你保持兴趣、充满动力,时刻知道学的东西能用在哪、能怎么用。</span> </p> <p style="color:#666666;"> <span style="font-size:14px;">平时不明白的知识点,放在项目里去理解就恍然大悟了。</span> </p> <p style="color:#666666;"> <span></span> </p> <p style="color:#666666;"> <span style="font-size:14px;"> </span> </p> <p style="color:#666666;"> <span style="color:#FF0000;font-size:14px;"><strong>一、融汇贯通</strong></span> </p> <p style="color:#666666;"> <span style="font-size:14px;">本视频采用了前后端分离的开发模式,前端使用Vue.js+Element UI实现了Web页面的呈现,后端使用Python 的Django框架实现了数据访问的接口,前端通过Axios访问后端接口获得数据。在学习完本章节后,真正理解前后端的各自承担的工作。</span> </p> <p style="color:#666666;"> <span style="font-size:14px;"> </span> </p> <p style="color:#666666;"> <span style="color:#FF0000;font-size:14px;"><strong>二、贴近实战</strong></span> </p> <p style="color:#666666;"> <span style="font-size:14px;">本系列课程为练手项目实战:学生管理系统v4.0的开发,项目包含了如下几个内容:项目的总体介绍、基本功能的演示、Vuejs的初始化、Element UI的使用、在Django中实现针对数据的增删改查的接口、在Vuejs中实现前端增删改查的调用、实现文件的上传、实现表格的分页、实现导出数据到Excel、实现通过Excel导入数据、实现针对表格的批量化操作等等,所有的功能都通过演示完成、贴近了实战</span> </p> <p style="color:#666666;"> <span style="font-size:14px;"> </span> </p> <p style="color:#666666;"> <span style="color:#FF0000;font-size:14px;"><strong>三、课程亮点</strong></span> </p> <p style="color:#666666;"> <span style="font-size:14px;">在本案例中,最大的亮点在于前后端做了分离,真正理解前后端的各自承担的工作。前端如何和后端交互</span> </p> <p style="color:#666666;"> <span style="font-size:14px;"> </span> </p> <p style="color:#666666;"> <span style="color:#FF0000;font-size:14px;"><strong>适合人群:</strong></span> </p> <p style="color:#666666;"> <span style="font-size:14px;">1、有Python语言基础、web前端基础,想要深入学习Python Web框架的朋友;</span> </p> <p style="color:#666666;"> <span style="font-size:14px;">2、有Django基础,但是想学习企业级项目实战的朋友;</span> </p> <p style="color:#666666;"> <span style="font-size:14px;">3、有MySQL数据库基础的朋友</span> </p> <p style="color:#666666;"> <span style="font-size:14px;"> </span> </p> <p style="color:#666666;"> <span style="font-size:14px;"><img alt="" src="https://img-bss.csdnimg.cn/202009070752197496.png" /><br /> </span> </p> <p style="color:#666666;"> <span style="font-size:14px;"><br /> </span> </p>
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值