ActivityTransitionって何
Google Developersの「アクティビティ遷移をカスタマイズする」によれば、
マテリアル デザイン アプリの Activity transitions (アクティビティ遷移)では、共通する要素の間での動作や変化を通じて、状態の切り替えに視覚的なつながりを持たせます。
つまり、前後のActivityで共通する要素がある場合などには、スムーズな切り替えを提示してあげよう!ということでしょうか。
注意しなければならないのが、これらを使えるのが__APIレベル21(5.0 Lollipop)以上から__であること。バックポートライブラリも提供されてないので、minSdkVersion
によっては処理を分岐させる必要がありますね。
ActivityTransitionの種類
アクティビティ遷移にもいくつか種類があります。
Enter
どのようにして画面に__登場__するか。
Activityが表示される際のアニメーションをつけたい場合に使います。
Exit
どのようにして画面から__退場__するか。
画面遷移の発生などでActivityから離れる際のアニメーションをつける場合に使います。
Shared Elements
画面上にある共通の要素(例えばImageViewとか)をどのようにアニメーションさせるか。
PlayStoreの例がわかりやすいでしょうか。
アプリアイコンが前後のAcitivityで共通要素で、画面遷移時にはアイコンが動くようなアニメーションが入ってます。
このようにちょっとしたアニメーションが加わるだけでも、見た目が大きく変わります
ガイドに沿って上手に活用すれば、ユーザの注視点をうまく誘導させることができたりと、効果的であることは間違いなさそう
Shared Elements Transitionの実装
Lv.1: 一番簡単な導入
導入そのものはとても簡単です!
1.WINDOW_CONTENT_TRANSITION
を有効にする
WINDOW_CONTENT_TRANSITION
を有効にするサンプルを実装していて気がついたのですが、これする必要が__なさそう__なのです
参考までに、有効にするには
- style.xmlなどで
windowContentTransitions
をtrue
にする
<item name="android:windowContentTransitions">true</item>
- activityから
requestFeature()
で有効にする
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
2.アニメーション対象viewを指定する
Shared Element Transitionでは、transitionName
というviewのアトリビュートを使ってどのviewが共通要素なのかを指定しています。このtransitionName
の指定は、__レイアウトxmlに直接記述する方法__と__コードから動的に指定する方法__があります。
- レイアウトxmlに直接記述する方法
android:transitionName
に識別可能な文字列を指定する
<ImageView
android:id="@+id/image"
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@mipmap/ic_launcher"
android:transitionName="image" <-ここが大事
/>
<ImageView
android:id="@+id/image"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_alignParentEnd="true"
android:src="@mipmap/ic_launcher"
android:transitionName="image" <-ここが大事
/>
- コードから動的に指定する方法
View#setTransitionName()
を使う
//add transition name (= android:transitionName)
binding.image.setTransitionName("image_code");
3.Intentに必要な情報を詰めて遷移先に渡す
アニメーションさせるviewの指定が済んだら、必要な情報をIntentに乗っけます。
必要な情報とはActivityOptions
のことで、Activity
ならActivityOptions
、AppCompatActivity
ならActivityOptionsCompat
を使います。各クラスに実装されているstaticメソッドmakeSceneTransitionAnimation()
でActivityOptions
インスタンスが取得できます。
ActivityOptionsCompat compat =
ActivityOptionsCompat.makeSceneTransitionAnimation(this, binding.image,
binding.image.getTransitionName());
ActivityOptionsCompat
の準備ができたらstartActivity(Context,Bundle)
で遷移を開始します。
startActivity(new Intent(this, SimpleSharedCodeSecondActivity.class), compat.toBundle());
ActivityOptionsCompat
はtoBundle()
でBundle型にして第2引数に渡します。
サンプル例
下の2つが簡単な導入例です。
transitionsamples/simple_sharedではアニメーション対象viewをレイアウトXMLで指定しています。
transitionsamples/simple_shared_codeではアニメーション対象viewをコードから指定しています。
Lv.2: custom transitionを定義して使う
ここからが本番。
今回、上にあるPlayStoreのgifのようなアニメーションを実現したかったのだけど、問題発生。
適用したいシチュエーション
- PlayStoreのような一覧から詳細に遷移する場面
- 遷移元と遷移先には同じ画像が使われている
- ただし、解像度が違う(遷移元:低画質、遷移先:高画質)ため、遷移時に取得する必要がある
- 画像はアプリ内にresourceとしてあるのではなく、Glideを用いてサーバから取得する
起きてしまった問題
-
遷移アニメーション中に画像サイズがおかしくなる
- じゃあ画像の取得が終わるまで待てばいいじゃん
-
画像のロードが終わるまで遷移が始まらなくて固まってるように見える
- かといって低画質画像を使うわけにはいかない
- うーん困った
- かといって低画質画像を使うわけにはいかない
-
画像のロードが終わるまで遷移が始まらなくて固まってるように見える
- じゃあ画像の取得が終わるまで待てばいいじゃん
対策案: PlayStoreをマネてみよう
あくまで推測ですが、
- 遷移元のアイコンと遷移先のヘッダー画像は別物で、かつネットワークからの取得が必要
- 一旦丸い図形(ピンクの丸)に変形させることで、操作への反応速度を保証
- ヘッダー画像が取れ次第ヘッダー画像をフェードインさせることで、スムーズな遷移を演出
Googleが導いた解を信じ、このアニメーションの実現を目指します。
Transitionのカスタム実装
Google Developersの解説を参考にしながら実装します。
Transitionを継承して自作するときには、少なくとも以下の3メソッドをoverrideします。
public class CustomTransition extends Transition {
@Override
public void captureStartValues(TransitionValues values) {}
@Override
public void captureEndValues(TransitionValues values) {}
@Override
public Animator createAnimator(ViewGroup sceneRoot,
TransitionValues startValues,
TransitionValues endValues) {}
}
でもこれだけではさっぱりわからないので、Fade
クラスを覗いてみます。
...
@Override
public void captureStartValues(TransitionValues transitionValues) {
super.captureStartValues(transitionValues);
transitionValues.values.put(PROPNAME_TRANSITION_ALPHA,
transitionValues.view.getTransitionAlpha());
}
...
private Animator createAnimation(final View view, float startAlpha, final float endAlpha) {
if (startAlpha == endAlpha) {
return null;
}
view.setTransitionAlpha(startAlpha);
final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "transitionAlpha", endAlpha);
if (DBG) {
Log.d(LOG_TAG, "Created animator " + anim);
}
final FadeAnimatorListener listener = new FadeAnimatorListener(view);
anim.addListener(listener);
addListener(new TransitionListenerAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
view.setTransitionAlpha(1);
}
});
return anim;
}
...
どうやら、captureStartValues
に渡ってきた値を保存して、createAnimation
で最終的なアニメーションを生成して返しているっぽいです。
実装したコード全体はgithubで見ていただければと思いますが、重要なのはcreateAnimation
内でのアニメーション生成部分。
...
//fadeするアニメーション
Animator fadeOutStartView = ObjectAnimator.ofFloat(startView, View.ALPHA, 1, 0);
Animator fadeInStartBallView = ObjectAnimator.ofFloat(startBallView, View.ALPHA, 0, 1);
Animator fadeInEndView = ObjectAnimator.ofFloat(endView, View.ALPHA, 0, 1);
Animator fadeOutEndBallView = ObjectAnimator.ofFloat(endBallView, View.ALPHA, 1, 0);
...
//移動するアニメーション
//はじめとおわりの位置を計算(対象viewの中央)
Animator moveStartView =
ObjectAnimator.ofFloat(startView, View.TRANSLATION_X, View.TRANSLATION_Y,
getPathMotion().getPath(transStartX, transStartY, transEndX, transEndY));
Animator moveStartBallView =
ObjectAnimator.ofFloat(startBallView, View.TRANSLATION_X, View.TRANSLATION_Y,
getPathMotion().getPath(transStartX, transStartY, transEndX, transEndY));
...
//step1: 移動元Viewを丸に変形しながら、移動先Viewの中央まで移動しつつ、画像はフェードアウト、背景フェードイン
transitionStep1.playTogether(revealStartView, revealStartBallView, moveStartView,
moveStartBallView, fadeOutStartView, fadeInStartBallView);
...
ここでの最終的な返り値はAnimator
型です。
アニメーション対象のviewや一時的に登場するviewに対してAnimator
でアニメーションを個別に定義してあげ、それらをうまく組み合わせることで一連のアニメーションを実現することができます。個々のAnimatorの使い方については説明を省きますが、viewなどの移動、変形、透過などができます。
今回実装したPlayStoreTransition
は、3つのアニメーションを逐次実行しています。
- 遷移元viewを円形の図形に変形&フェードアウトさせながら、遷移先viewの中心へ移動
- 円形の図形を遷移先view全体が隠れるまで拡大させる
- 遷移先viewをフェードインさせる
実際にアニメーションさせてみます。
とてもいい感じです。
あとはこれを画像取得のタイミングとうまくリンクさせれば良さそうです。
番外: ステータスバーをアニメーション対象外にしたい
excludeTarget
を利用することで、指定したviewを対象外とすることができます。
サンプルでは、XMLで除外定義しています。
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<targets>
<!- ここから ->
<target android:excludeId="@android:id/statusBarBackground" />
<target android:excludeId="@android:id/navigationBarBackground" />
<!- ここまで ->
</targets>
<fade />
</transitionSet>
この状態だと、ステータスバーやナビゲーションバーに変化がつかなくなります。
gifのステータスバー(一番上の通知とか出る部分)とナビゲーションバー(一番下のホームキーとかの部分)に注目して見てもらえれば違いがわかると思います
サンプルコードとか
実装中に、PlayStoreっぽいトランジションの先人がいることに気がつきました。
だいぶ参考にさせていただいてます。
https://github.com/naman14/PlayAnimations
また、https://github.com/satsukies/AnimationSamplesに自分のサンプルコードもあります。
最後に
これだけ説明しておいてアレですが、実はこれではまだ解決しませんでした。
というのも、アニメーションの継ぎ目で__viewがちらついてしまったり、隠しているはずの部分が見えてしまったり__していて、このままではバグっぽく見えるのです。
OMG...
RAMに余裕のある端末やCPUパワーのある端末では起きにくいですが、実用するとなればそれなりのスペックの端末も存在するので、最適化なり別の方法なりを考える必要がありそう。