Posted at

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

More than 5 years have passed since last update.

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


ソースコードの取得

Google I/O 2014アプリのソースコード公開については、Android Developers Blogで紹介されていますが、日本語訳の記事もGoogle Developer Relations Japan Blogでも公開されています。

ソースコード自体はGitHubで公開されているので、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))もありました。


リスナーの設定


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の表示を切り替えてるようです。


初期化


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 = 152dpmActionBarAutoHideSensivity = 48dpとなっていました。

MinYの方は表示を切り替える際のしきい値のようです。Sensivityの方はイベントを拾うかどうかのスクロール量のしきい値みたいです。

この2つの値をベースに後述するスクロール量の計算を行っています。


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


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);
}


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


Android-L以前のための処理


BaseActivity.java

    protected void autoShowOrHideActionBar(boolean show) {

if (show == mActionBarShown) {
return;
}

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



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()が、以下のようになってました。


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まわりの表示切り替え処理


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の書き方も、かなり参考になりました。