ActionBarをスクロールに合わせて表示したり非表示にしたりする


MaterialDesignを適用したAndroidアプリのお手本として公開されているGoogle I/O 2014の公式アプリのソースコードから、ActionBarまわりのUIについて参考にしたい部分のコードを読んでみました。

## ソースコードの取得

Google I/O 2014アプリのソースコード公開については、[Android Developers Blog](http://android-developers.blogspot.jp/2014/08/material-design-in-2014-google-io-app.html)で紹介されていますが、日本語訳の記事も[Google Developer Relations Japan Blog](http://googledevjp.blogspot.jp/2014/08/io-2014.html)でも公開されています。

ソースコード自体は[GitHubで公開](https://github.com/google/iosched)されているので、cloneなりforkなりしてローカルにダウンロードして、AndroidStudioやElipseなどで見る方が見やすいです。

## 参考にしたソースファイル

Activityの共通処理の1つとして、リストのスクロールに合わせてActionBarを表示させたり非表示にしたりするアニメーションを行っているコードを中心に読みました。

今回参考にしたいコードは、`com.google.samples.apps.iosched.ui.BaseActivity.java`の(L:1152〜)辺りにありました。

## 処理の流れ

ざっくり見ると、初期化〜スクロール量の計算〜UI操作という流れのようです。（当たり前かもしれませんが。）
メソッドで追いかけると、

- L:1198 : protected void enableActionBarAutoHide(final ListView listView)
- L:1152 : private void initActionBarAutoHide()
- L:1168 : private void onMainContentScrolled(int currentY, int deltaY)
- L:1188 : protected void autoShowOrHideActionBar(boolean show)
- L:1434 : protected void onActionBarAutoShowOrHide(boolean shown)

という流れです。
途中、Android-Lとそれ以前との互換性を保つ処理(`getLPreviewUtils().showHideActionBarIfPartOfDecor(show)`)もありました。

## リスナーの設定

```java:BaseActivity.java
    protected void enableActionBarAutoHide(final ListView listView) {
        initActionBarAutoHide();
        listView.setOnScrollListener(new AbsListView.OnScrollListener() {

			〜省略〜

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                onMainContentScrolled(firstVisibleItem <= ITEMS_THRESHOLD ? 0 : Integer.MAX_VALUE,
                        lastFvi - firstVisibleItem > 0 ? Integer.MIN_VALUE :
                                lastFvi == firstVisibleItem ? 0 : Integer.MAX_VALUE
                );
                lastFvi = firstVisibleItem;
            }
        });
    }
```

ListViewのOnScrollイベントの中で、リストのindexの差分からActionBarの表示を切り替えてるようです。

## 初期化

```java:BaseActivity.java
    /**
     * Initializes the Action Bar auto-hide (aka Quick Recall) effect.
     */
    private void initActionBarAutoHide() {
        mActionBarAutoHideEnabled = true;
        mActionBarAutoHideMinY = getResources().getDimensionPixelSize(
                R.dimen.action_bar_auto_hide_min_y);
        mActionBarAutoHideSensivity = getResources().getDimensionPixelSize(
                R.dimen.action_bar_auto_hide_sensivity);
    }
```

ここで表示切り替えを行う基準となる数値を、設定しているようです。きちんとdimenリソースで管理されてます。
実際は、`mActionBarAutoHideMinY = 152dp`で`mActionBarAutoHideSensivity = 48dp`となっていました。

MinYの方は表示を切り替える際のしきい値のようです。Sensivityの方はイベントを拾うかどうかのスクロール量のしきい値みたいです。
この2つの値をベースに後述するスクロール量の計算を行っています。

## スクロール量から表示切り替えの判定

```java:BaseActivity.java
    /**
     * Indicates that the main content has scrolled (for the purposes of showing/hiding
     * the action bar for the "action bar auto hide" effect). currentY and deltaY may be exact
     * (if the underlying view supports it) or may be approximate indications:
     * deltaY may be INT_MAX to mean "scrolled forward indeterminately" and INT_MIN to mean
     * "scrolled backward indeterminately".  currentY may be 0 to mean "somewhere close to the
     * start of the list" and INT_MAX to mean "we don't know, but not at the start of the list"
     */
    private void onMainContentScrolled(int currentY, int deltaY) {
        if (deltaY > mActionBarAutoHideSensivity) {
            deltaY = mActionBarAutoHideSensivity;
        } else if (deltaY < -mActionBarAutoHideSensivity) {
            deltaY = -mActionBarAutoHideSensivity;
        }

        if (Math.signum(deltaY) * Math.signum(mActionBarAutoHideSignal) < 0) {
            // deltaY is a motion opposite to the accumulated signal, so reset signal
            mActionBarAutoHideSignal = deltaY;
        } else {
            // add to accumulated signal
            mActionBarAutoHideSignal += deltaY;
        }

        boolean shouldShow = currentY < mActionBarAutoHideMinY ||
                (mActionBarAutoHideSignal <= -mActionBarAutoHideSensivity);
        autoShowOrHideActionBar(shouldShow);
    }
```

コメントにあるように、`currentY`と`deltaY`は実際には "0", "Integer.MIN_VALUE", "Integer.MAX_VALUE" のどれかしか入ってきません。`Math.signum()`は引数の符号を返すメソッドなので、ユーザーがスクロールの上下を反転させたタイミングで、スクロール量の蓄積をリセットしてます。

## Android-L以前のための処理

```java:BaseActivity.java
    protected void autoShowOrHideActionBar(boolean show) {
        if (show == mActionBarShown) {
            return;
        }

        mActionBarShown = show;
        getLPreviewUtils().showHideActionBarIfPartOfDecor(show);
        onActionBarAutoShowOrHide(show);
    }
```

```java:LPreviewUtilsBase.java
    public void showHideActionBarIfPartOfDecor(boolean show) {
        // pre-L, action bar is always part of the window decor
        if (show) {
            mActivity.getActionBar().show();
        } else {
            mActivity.getActionBar().hide();
        }
    }
```

Android4.4までは、`ActionBar.show()`か`ActionBar.hide()`を呼ばないと切り替えられないので、API Levelによってコードが別になってました。上記がAndroid-L以前のための処理です。

Android-Lだとビルド時に"src/main"ではなく、"src/lpreview"以下のソースになり同じ`showHideActionBarIfPartOfDecor()`が、以下のようになってました。

```java:LPreviewUtilsImpl.java
    @Override
    public void showHideActionBarIfPartOfDecor(boolean show) {
        if (mActionBarToolbar != null) {
            // Action bar is part of the layout
            return;
        }

        // Action bar is part of window decor
        super.showHideActionBarIfPartOfDecor(show);
    }
```

## ActionBarまわりの表示切り替え処理

```java:BaseActivity.java
    protected void onActionBarAutoShowOrHide(boolean shown) {
        if (mStatusBarColorAnimator != null) {
            mStatusBarColorAnimator.cancel();
        }
        mStatusBarColorAnimator = ObjectAnimator.ofInt(mLPreviewUtils, "statusBarColor",
                shown ? mThemedStatusBarColor : Color.BLACK).setDuration(250);
        mStatusBarColorAnimator.setEvaluator(ARGB_EVALUATOR);
        mStatusBarColorAnimator.start();

        updateSwipeRefreshProgressBarTop();

        for (View view : mHideableHeaderViews) {
            if (shown) {
                view.animate()
                        .translationY(0)
                        .alpha(1)
                        .setDuration(HEADER_HIDE_ANIM_DURATION)
                        .setInterpolator(new DecelerateInterpolator());
            } else {
                view.animate()
                        .translationY(-view.getBottom())
                        .alpha(0)
                        .setDuration(HEADER_HIDE_ANIM_DURATION)
                        .setInterpolator(new DecelerateInterpolator());
            }
        }
    }
```

最後に表示を切り替えるViewにアニメーションを適用しているのですが、面白いなーと思ったのが滑らかさの演出の部分です。ちゃんとViewのまとまり毎にアニメーションをセットしていて、折りたたむようにViewが出たり引っ込んだりするように見せてるところです。

## まとめ

コードを読んでみて、スクロール方向が反転するときの判定が特に参考になったなーと思います。他にも、上記では触れてませんがAndroid-Lとそれ以前のビルドを切り替えるための`build.gradle`の書き方も、かなり参考になりました。