LoginSignup
212
210

More than 5 years have passed since last update.

Google Playストアアプリのようにスクロール時にActionBarやToolbarの表示を切り替える

Posted at

Android LollipopでToolbarが導入されていますが、
Toolbarはビューの操作と連動した動きをさせたいケースが出てくると思います。
実際、Material Designになった新しいPlayストアアプリなどでは、以下のような挙動になっています。

  • スクロールに連動してバーの表示が切り替わる
  • スクロールによってバーは消えるがタブは常に残る
  • バーの表示/非表示が完全に切り替わらない位置で手を離すと、アニメーションで完全に切り替わる

これをAppCompat-v7とAndroid-ObservableScrollViewで実装する方法の説明です。

play.gif

ライブラリの導入

スクロールと連動させたいときに困るのが、ListView/ScrollView/WebViewといったスクロール可能な部品は、いずれもスクロール位置を外部から取得できないことです。
romannurik-codeのscrolltricksというサンプルを参考にすると、スクロールの扱い方が分かってきますが…やはり複雑です。
しかも、ListView/ScrollView/WebViewは独立した実装のため、それぞれ制御する必要があります。

そこで、これらの制御をまとめたライブラリAndroid-ObservableScrollViewを作ってみましたので、これを使って実現します。

build.gradle
dependencies {
    compile 'com.android.support:appcompat-v7:21.0.0'
    compile 'com.github.ksoichiro:android-observablescrollview:1.0.0'
}

※以下のサンプルコードは、(日本語コメント以外は)すべて上記のライブラリに含まれています。

実装例

ActionBarの表示を切り替える

概要

下にスクロールするとActionBarが隠れ、上にスクロールすると表示されるパターンです。
正確には、スクロールと完全に同期している訳ではなく、スクロール後にActionBarshow()/hide()を実行しています。

demo1.gif
demo2.gif
demo3.gif

レイアウト

ライブラリのListViewを定義します。
ScrollView/WebViewは、それぞれ対応するクラスがありますので読み替えてください。

res/layout/activity_actionbarcontrollistview.xml
<com.github.ksoichiro.android.observablescrollview.ObservableListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Activityでの制御

Activityでは、そのListViewにコールバックを設定し、スクロール方向に応じた処理を実装します。

ActionBarControlListViewActivity.java
public class ActionBarControlListViewActivity extends ActionBarActivity implements ObservableScrollViewCallbacks {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_actionbarcontrollistview);

        ObservableListView listView = (ObservableListView) findViewById(R.id.list);
        listView.setScrollViewCallbacks(this);
        :
    }

    @Override
    public void onUpOrCancelMotionEvent(ScrollState scrollState) {
        ActionBar ab = getSupportActionBar();
        if (scrollState == ScrollState.UP) {
            if (ab.isShowing()) {
                ab.hide();
            }
        } else if (scrollState == ScrollState.DOWN) {
            if (!ab.isShowing()) {
                ab.show();
            }
        }
    }
}

Toolbarの表示を切り替えつつ、Toolbar下のビューは常に残す

概要

こちらが本題ですが、スクロール方向に応じたアニメーションだけでなく、スクロールと連動させてToolbarを同じように移動させるには、さらにもう一工夫必要になります。
コンテンツとToolbarの位置を一緒にスクロールさせるために、スクロール部分の上部に余白を追加することと、FrameLayoutで重ね合わせて表示するところがポイントです。

ListViewで説明しますが、ScrollViewでも同じです。
WebViewの場合はさらに工夫が必要です。(後述)

demo4.gif
demo5.gif

レイアウト

以下のように、ライブラリのObservableListViewとAppCompat-v7のToolbarFrameLayoutで並べて定義します。
ただしToolbarの方は、固定で表示するビューとセットで操作するため、LinearLayoutで囲っています。
また、Material Designにおける"影"はElevationで設定するのでしょうが、通常のビューでの表示ができない(たぶん..)ためImageViewで影を付けています。

res/layout/activity_toolbarcontrollistview.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- ライブラリの部品を使用 -->
    <com.github.ksoichiro.android.observablescrollview.ObservableListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- Toolbarと固定の部品を包含するレイアウト -->
    <LinearLayout
        android:id="@+id/header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- backgroundはここで設定しないとちらつきが発生する -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/primary"
            android:orientation="vertical">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="?attr/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                app:popupTheme="@style/Theme.AppCompat.Light.DarkActionBar"
                app:theme="@style/Toolbar" />

            <!-- スクロールしても表示し続けるビュー -->
            <TextView
                android:id="@+id/sticky"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:gravity="center_vertical"
                android:paddingLeft="@dimen/margin_standard"
                android:paddingRight="@dimen/margin_standard"
                android:text="@string/sticky_header"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@android:color/white" />
        </LinearLayout>

        <!-- ActionBar等と同様に影をつける(Elevationの代わり) -->
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="@dimen/toolbar_elevation"
            android:src="@drawable/shadow" />
    </LinearLayout>
</FrameLayout>

Activityでの制御

このレイアウトに対し、Activityではスクロールのコールバックを使った位置変更やアニメーションを以下のように実装します。
ドラッグ中はスクロールに連動してヘッダ部分を動かし、手を離した後はアニメーションでToolbarを表示または非表示にします。

ToolbarControlListViewActivity.java
public class ToolbarControlListViewActivity extends ActionBarActivity implements ObservableScrollViewCallbacks {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_toolbarcontrollistview);

        // ToolbarをActionBarとして設定(.NoActionBarのテーマが必要)
        setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
        :
        mListView = (ObservableListView) findViewById(R.id.list);
        mListView.setScrollViewCallbacks(this);

        // ListViewのヘッダにHeader(Toolbar + StickyView)分の余白を追加
        LayoutInflater inflater = LayoutInflater.from(this);
        mListView.addHeaderView(inflater.inflate(R.layout.padding, null)); // toolbar
        mListView.addHeaderView(inflater.inflate(R.layout.padding, null)); // sticky view
        :
    }

    @Override
    public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
        // ドラッグ中のみ反応させ、手を離した後はアニメーションに任せる
        if (dragging) {
            int toolbarHeight = mToolbarView.getHeight();
            if (firstScroll) {
                // ある程度スクロールした状態から動かすときは現在のスクロール位置を基準にする
                float currentHeaderTranslationY = ViewHelper.getTranslationY(mHeaderView);
                if (-toolbarHeight < currentHeaderTranslationY && toolbarHeight < scrollY) {
                    mBaseTranslationY = scrollY;
                }
            }
            // Toolbarの可動範囲を-toolbarHeightから0までに制限する
            int headerTranslationY = Math.min(0, Math.max(-toolbarHeight, -(scrollY - mBaseTranslationY)));

            // 動作中のアニメーションをキャンセルして移動
            ViewPropertyAnimator.animate(mHeaderView).cancel();
            ViewHelper.setTranslationY(mHeaderView, headerTranslationY);
        }
    }
    :
    @Override
    public void onUpOrCancelMotionEvent(ScrollState scrollState) {
        mBaseTranslationY = 0;

        float headerTranslationY = ViewHelper.getTranslationY(mHeaderView);
        int toolbarHeight = mToolbarView.getHeight();
        if (scrollState == ScrollState.UP) {
            // Toolbarを隠す
            if (toolbarHeight < mListView.getCurrentScrollY()) {
                if (headerTranslationY != -toolbarHeight) {
                    ViewPropertyAnimator.animate(mHeaderView).cancel();
                    ViewPropertyAnimator.animate(mHeaderView).translationY(-toolbarHeight).setDuration(200).start();
                }
            }
        } else if (scrollState == ScrollState.DOWN) {
            // Toolbarを表示する
            if (toolbarHeight < mListView.getCurrentScrollY()) {
                if (headerTranslationY != 0) {
                    ViewPropertyAnimator.animate(mHeaderView).cancel();
                    ViewPropertyAnimator.animate(mHeaderView).translationY(0).setDuration(200).start();
                }
            }
        }
    }
}

なお、Android 3.0未満も対象にしたコードになっているため、アニメーションや位置変更にはJakeWharton/NineOldAndroidsを使用しています。
Android 3.0+ のみであれば、setTranslationY()などはViewに対して直接呼び出せますので、読み替えてください。

Toolbarの表示を切り替えつつ、Toolbar下のビューは常に残す(WebViewの場合)

概要

上述の通り、WebViewではListViewScrollViewと違う実装になります。
上記のListViewScrollViewは、位置調整のためにヘッダの高さに相当する余白を入れているのがポイントなのですが、WebViewの場合は上部に余白を入れることができないようです。
そのため、ObservableScrollViewObservableWebViewを包み、2つのコールバックを使って制御することで実現します。
ObservableWebView単独でスクロール中に位置(translationY)を動かす方法も考えられますが、これをやるとスクロールがガタガタになってしまいます…。

demo6.gif

レイアウト

res/layout/activity_toolbarcontrolwebview.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.github.ksoichiro.android.observablescrollview.ObservableScrollView
        android:id="@+id/scroll"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <!-- WebView上部に余白が入れられないため、ScrollViewにheader分の余白を追加 -->
            <View
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:minHeight="?attr/actionBarSize" />
            <View
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:minHeight="?attr/actionBarSize" />

            <com.github.ksoichiro.android.observablescrollview.ObservableWebView
                android:id="@+id/web"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>
    </com.github.ksoichiro.android.observablescrollview.ObservableScrollView>

    <!-- 以下はListView/ScrollViewの場合と同じ -->
    <LinearLayout
        android:id="@+id/header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/primary"
            android:orientation="vertical">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="?attr/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                app:popupTheme="@style/Theme.AppCompat.Light.DarkActionBar"
                app:theme="@style/Toolbar" />

            <TextView
                android:id="@+id/sticky"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:gravity="center_vertical"
                android:paddingLeft="@dimen/margin_standard"
                android:paddingRight="@dimen/margin_standard"
                android:text="@string/sticky_header"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@android:color/white" />
        </LinearLayout>

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="@dimen/toolbar_elevation"
            android:src="@drawable/shadow" />
    </LinearLayout>
</FrameLayout>

Activityでの制御

ListView/ScrollViewの場合は、ACTION_DOWNのイベントでドラッグの開始を判定していました。
WebViewScrollViewを組み合わせた場合は、ACTION_DOWNイベントがScrollViewで発生し、スクロールイベントとドラッグ終了(ACTION_UP/ACTION_CANCEL)はWebViewで発生してしまいます。
そのため、ObservableScrollViewObservableWebViewのそれぞれのコールバックを実装してドラッグ状態を判定します。

ToolbarControlWebViewActivity.java
public class ToolbarControlWebViewActivity extends ActionBarActivity {
    :
    private ObservableScrollViewCallbacks mWebViewScrollCallbacks = new ObservableScrollViewCallbacks() {
        @Override
        public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
        }

        @Override
        public void onDownMotionEvent() {
            // ScrollViewにWebViewを入れた場合、ACTION_DOWNのイベントが
            // WebViewにしかないため、WebViewのコールバック内で
            // ドラッグ開始を判定する
            mFirstScroll = mDragging = true;
        }

        @Override
        public void onUpOrCancelMotionEvent(ScrollState scrollState) {
        }
    };

    private ObservableScrollViewCallbacks mScrollViewScrollCallbacks = new ObservableScrollViewCallbacks() {
        @Override
        public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
            // ライブラリでなくこのクラスで定義したフラグ(mDragging, mFirstScroll)で制御する
            if (mDragging) {
                int toolbarHeight = mToolbarView.getHeight();
                if (mFirstScroll) {
                    mFirstScroll = false;
                    float currentHeaderTranslationY = ViewHelper.getTranslationY(mHeaderView);
                    if (-toolbarHeight < currentHeaderTranslationY && toolbarHeight < scrollY) {
                        mBaseTranslationY = scrollY;
                    }
                }
                int headerTranslationY = Math.min(0, Math.max(-toolbarHeight, -(scrollY - mBaseTranslationY)));
                ViewPropertyAnimator.animate(mHeaderView).cancel();
                ViewHelper.setTranslationY(mHeaderView, headerTranslationY);
            }
        }

        @Override
        public void onDownMotionEvent() {
        }

        @Override
        public void onUpOrCancelMotionEvent(ScrollState scrollState) {
            // ドラッグ終了はWebViewでのハンドリングが必要
            mDragging = false;
            // 以下はListView/ScrollViewの場合と同じ
            :
        }
    };
}

以上です。

212
210
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
212
210