時代はSnackbarだぜぇーといってみたものの(。・_・。)の第2段(^o^)v
Snackbarっぽく表示するには下からViewをびにょ〜んと生やしたりうにょ~んとへっこまさないといけないんだけど、Androidの普通のアニメーションでは見た目だけが変わるだけで実際のViewの大きさなどは変更してくれず、他のViewを自動的にリサイズ/再配置したりとかは出来ないので、Viewの実際のサイズを変更するために専用のAnimationを作ってみたぜ(^o^)vというのが前回の記事。
Snackbarは思った通りに動かんのんでオレオレ実装してやったぜぇーって記事なのでSnackbarでええやんとかいうコメントはいらんねんで。
前回作ったAnimation(ResizeAinimation)なんだけど、これだけではSnackbarっぽい挙動にはならず、実際にはレイアウトファイルとコードが必要なので、今回はそこら辺をシェア。
レイアウトはこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" tools:ignore="MergeRootFrame"> ... <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@+id/message_panel"/> <include layout="@layout/panel_message" android:id="@+id/message_panel" /> </RelativeLayout> |
id==containerのViewはFragmentを入れるためのプレースホルダー用のViewGroupですが、別に普通のViewでもなんでもいいです。
id== panel_messageのViewがSnackbarっぽく表示したいViewですが、こちらも特に縛りはなくなんでもいいです。まぁ普通はTextViewかTextViewを入れたViewGroupでしょうけど。
前回書いたとおりcontainerに入っているFragmentを押しのけて重ならないようにpanel_messageのViewを表示したいので、今回は外側をRelativeLayoutにしていますがこれも特に縛りはないです。重なるように表示したければFrameLayoutでもなんでも好きにすればよいです。
押しのけ動作のためにpanel_messageのViewの大きさが変わるようにRelativeLayoutを使って押しのけられる側(container)にandroid:layout_aboveを指定していますが、こちらもLineaLayoutでもかまわないです。
ようは自分が思った通りのレイアウトファイルであればいいのです。ただしSnackbar的に見せるにはSnackbar代わりのViewを相対的に一番下にしといたほうが良いです。
でもこのレイアウトにしただけではpanel_messageの表示非表示を切り替えてもびにょ〜んうにょ〜んと動いてくれませんのでコードが必要です。
じゃじゃん♪
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
private static final int DURATION_RESIZE_MS = 300; ... private View mRootView; private View mMessagePanelView; private TextView mMessageTv; ... /** * show message panel * @param message * @param autoHideDurationMs if greater than zero, * automatically hide message panel after specific duration */ protected void showMessage(final String message, final long autoHideDurationMs) { mMessageTv.setText(message); runOnUiThread(new Runnable() { @Override public void run() { mMessagePanelView.clearAnimation(); final ResizeAnimation expandAnimation = new ResizeAnimation(mMessagePanelView, mRootView.getWidth(), 0, mRootView.getWidth(), getResources().getDimensionPixelSize(R.dimen.bottom_message_panel_height)); expandAnimation.setDuration(DURATION_RESIZE_MS); expandAnimation.setAnimationListener(mAnimationListener); mMessagePanelView.setTag(R.id.visibility, 1); mMessagePanelView.setTag(R.id.auto_hide_duration, autoHideDurationMs); mMessagePanelView.setVisibility(View.VISIBLE); mMessagePanelView.startAnimation(expandAnimation); } }); } /** * hide message panel * @param animation * @param durationMs */ protected void hideMessage(final boolean animation, final long durationMs) { if (mMessagePanelView == null) return; runOnUiThread(new Runnable() { @Override public void run() { if (mMessagePanelView.getVisibility() == View.VISIBLE) { mMessagePanelView.clearAnimation(); if (animation) { final ResizeAnimation collapseAnimation = new ResizeAnimation(mMessagePanelView, mRootView.getWidth(), mMessagePanelView.getHeight(), mRootView.getWidth(), 0); collapseAnimation.setDuration(durationMs); collapseAnimation.setAnimationListener(mAnimationListener); mMessagePanelView.setTag(R.id.visibility, 0); mMessagePanelView.startAnimation(collapseAnimation); } else { // hide immediately mMessagePanelView.setVisibility(View.GONE); } } } }); } |
mMessagePanelViewは先ほどのid== panel_messageのViewです。mMessageTvは載せてないけどmMessagePanelView内にあるメッセージ表示用のTextViewです。
mRootViewは本来はいらないのですがActivity全体のViewです。今回の用途ではアニメーション時にViewの幅が必要になるのですが、呼び出すタイミングによってはViewのサイズが確定しておらずおかしな動作をしてしまうことがあるので、アニメーション開始時に毎回親のViewサイズを指定するようにしています。
R.dimen.bottom_message_panel_heightはSnackbarっぽく表示したいView(message_panel)の高さを指定するディメンジョンリソースです。
説明しなくてもわかるでしょうから説明は省略(*ノω・*)テヘじゃブログにならないので、簡単に説明すると、
まずはじめに、他のアニメーションが動いている時はわけわからなくなるのでView#clearAnimationで一旦アニメーションを止めます。動いているアニメーションがなくてもエラーにはならないので大丈夫。
つぎに、アニメーション用のAnimationインスタンスを生成します。生成するのは前回載せたResizeAnimationクラスで、コンストラクタにターゲットのViewとアニメーション開始時の幅と高さ&終了時の幅と高さを指定します。
1 2 3 4 5 |
final ResizeAnimation expandAnimation = new ResizeAnimation(mMessagePanelView, mRootView.getWidth(), 0, mRootView.getWidth(), getResources().getDimensionPixelSize(R.dimen.bottom_message_panel_height)); |
本来は毎回生成しなくてもいいのですが、諸般の事情…呼び出しのタイミングによってViewの幅・高さが未確定の場合でも大丈夫なように毎回生成しています。アニメーション開始が必ずViewのサイズ確定後であると保証できるのであれば1回だけ(例えばOnResumeとかで)生成するようにしてもかまいません。
Animation#setDurationはアニメーションにかかる時間を指定します。これは必須です。今回はSnackbarぽく300ミリ秒にしています。表示するViewの大きさ(高さ)によっては250ミリ秒ぐらいでもいいかもしれません。
1 2 |
mMessagePanelView.setTag(R.id.visibility, 1); mMessagePanelView.setTag(R.id.auto_hide_duration, autoHideDurationMs); |
この2行はアニメーションそのものとは関係ないですが、Snackbarぽく見せるための小細工です。表示なのか非表示なのか、表示した後に指定時間後に自動的に非表示にするかをView#setTagを使って保存しています。
それぞれのidはこんな感じでvalueリソースとして定義しておきます。
1 2 3 4 5 |
<?xml version="1.0" encoding="utf-8"?> <resources> <item type="id" name="auto_hide_duration" /> <item type="id" name="visibility" /> </resources> |
でもってあとはViewを表示してアニメーションを開始するだけです。
1 2 |
mMessagePanelView.setVisibility(View.VISIBLE); mMessagePanelView.startAnimation(expandAnimation); |
非表示にする時は、アニメーションのサイズ指定を逆にしてResizeAnimationを生成してView#startAnimationを呼び出します。
Snackbarぽい表示・非表示だけだと以上なのですが、ToastやSnackbarは一定時間後に自動的に隠れるように表示できますよね。せっかくSnackbarに似たアニメーションをするようにしたのでもうひと工夫しましょう。
ちゃららっちゃっちゃっちゃーん、あにめーしょんりすなぁー♪
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 |
/** * implementation of AnimationListener to show/hide message panel with animation */ private final Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() { @Override public void onAnimationStart(final Animation animation) { // ignore } @Override public void onAnimationEnd(final Animation animation) { final Object visibility = mMessagePanelView.getTag(R.id.visibility); if ((visibility instanceof Integer) && ((Integer)visibility == 1)) { final Object durationObj = mMessagePanelView.getTag(R.id.auto_hide_duration); final long duration = (durationObj instanceof Long) ? (Long)durationObj : 0; if (duration > 0) { queueEvent(new Runnable() { @Override public void run() { hideMessage(true, DURATION_RESIZE_MS); } }, duration); } } else { mMessagePanelView.setVisibility(View.GONE); } } @Override public void onAnimationRepeat(final Animation animation) { // ignore } }; |
AnimationListernerちゅうのをAnimation#setAnimationListenerでセットするとアニメーションの開始・終了・繰り返し時にコールバックとして呼び出してもらうことができます。
今回使うのは#onAnimationEndというアニメーション終了時のイベントコールバックです。
非表示にする時は、アニメーション終了時にView#setVisibility(View.GONE)を呼び出してView階層から抹殺します(しなくても高さが0になっているので見えないですが少しでもUI描画の負荷を下げるためにGONEにしています)
表示したときは、アニメーション終了時getTagで自動的に隠すかどうかを取得します。でもって自動的に隠す時はHandlerを使って指定時間後に非表示用にするためのメソッド#hideMessageを呼び出します。
ここらへんの処理をラップして1つのクラスにまとめたのがSnackbarになるわけですが、なかなか融通が効かないので、うまく動かねぇと何時間も悩むのであればちゃちゃっと車輪の再発明して自分で作ってしまったほうがよいのです。だって良い子の皆んななら前回のAnimation(ResizeAnimation)も含めて30-40分もあればこれぐらい作れるでしょ?
APIにあるからといって無理やりそれを使う必要はないんですよ、10分15分調べても思った通りに動かないなら、自分で作ってしまうのも選択肢の1つ。自分のしたいことを達成することが目的であってAPI/フレームワークを使うことは目的ではないのです。短期的・中期的・長期的にどの選択肢がいいかを考え選択すればいいのであって、世の中に既にあるから・車輪の再発明はだめだと固定概念で選択肢を減らしてもなにもいいことはないのです。
ということで今回はこれまで、さよならぁー(^.^)/~~~