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)
)もありました。
リスナーの設定
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の表示を切り替えてるようです。
初期化
/**
* 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つの値をベースに後述するスクロール量の計算を行っています。
スクロール量から表示切り替えの判定
/**
* 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以前のための処理
protected void autoShowOrHideActionBar(boolean show) {
if (show == mActionBarShown) {
return;
}
mActionBarShown = show;
getLPreviewUtils().showHideActionBarIfPartOfDecor(show);
onActionBarAutoShowOrHide(show);
}
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()
が、以下のようになってました。
@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まわりの表示切り替え処理
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
の書き方も、かなり参考になりました。