SharedElementとは
sharedElementを指定してActivityを起動すると、遷移前の画面の一部が拡大して遷移後の画面の一部になるようなtransitionが実現できます。Airbnbで詳細画面に遷移する時の写真の動きみたいなやつです。
実装方法については、Android Activity Transitions を実装するがわかりやすいです。
ただ、これはAPIv21以降じゃないと使えません。実装に使うActivityOptionsCompat.makeSceneTransitionAnimation
のコードを見ると、完全にv21未満は対象外って感じになってます。
public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity,
View sharedElement, String sharedElementName) {
if (Build.VERSION.SDK_INT >= 21) {
return new ActivityOptionsCompat.ActivityOptionsImpl21(
ActivityOptionsCompat21.makeSceneTransitionAnimation(activity, sharedElement, sharedElementName));
}
return new ActivityOptionsCompat();
}
どうやら独自で実装するしかなさそうだったので、Airbnbアプリの動きを参考にv21未満でも動くShared Elementっぽい動きを実装してみました。
サンプルアプリ
最終的にできたアプリはこちら。
https://github.com/konifar/ActivityAnimationSample
GridViewアイテムの写真をタップするとズームアップしてプレビューのActivityが表示されるだけのサンプルです。
以下、簡単に実装の説明です。
Activity起動時にtransition対象のViewの情報をintentにセット
アニメーションする対象のViewを渡して、Top、Leftの座標やWidth、Heightを渡します。
public static void start(Activity activity, View transitionView, PhotoModel model) {
Intent intent = new Intent(activity, DetailActivity.class);
int[] screenLocation = new int[2];
transitionView.getLocationOnScreen(screenLocation);
int orientation = activity.getResources().getConfiguration().orientation;
intent.putExtra(EXTRA_RESOURCE_ID, model.resId);
// 端末の向き
intent.putExtra(EXTRA_ORIENTATION, orientation);
// Left座標
intent.putExtra(EXTRA_LEFT, screenLocation[0]);
// Top座標
intent.putExtra(EXTRA_TOP, screenLocation[1]);
// Width
intent.putExtra(EXTRA_WIDTH, transitionView.getWidth());
// Height
intent.putExtra(EXTRA_HEIGHT, transitionView.getHeight());
activity.startActivity(intent);
// Activityのデフォルトtransitionを切る
activity.overridePendingTransition(0, 0);
}
起動後のActivityのonCreateでアニメーションを開始
渡されたデータをintentから取り出して、アニメーションを開始します。
ViewTreeObserver#onPreDraw()
を使って実装しているところがポイントで、こうすることでViewの構築前にAnimationを開始することができ、アニメーションをスムーズに表示することができます。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
ButterKnife.inject(this);
// 渡されたデータを取り出す
Bundle bundle = getIntent().getExtras();
final int resId = bundle.getInt(EXTRA_RESOURCE_ID);
final int thumbnailTop = bundle.getInt(EXTRA_TOP);
final int thumbnailLeft = bundle.getInt(EXTRA_LEFT);
final int thumbnailWidth = bundle.getInt(EXTRA_WIDTH);
final int thumbnailHeight = bundle.getInt(EXTRA_HEIGHT);
mOriginalOrientation = bundle.getInt(EXTRA_ORIENTATION);
mImgPreview.setImageResource(resId);
if (savedInstanceState == null) {
ViewTreeObserver observer = mImgPreview.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
mImgPreview.getViewTreeObserver().removeOnPreDrawListener(this);
int[] screenLocation = new int[2];
mImgPreview.getLocationOnScreen(screenLocation);
mLeftDelta = thumbnailLeft - screenLocation[0];
mTopDelta = thumbnailTop - screenLocation[1];
mWidthScale = (float) thumbnailWidth / (mImgPreview.getWidth());
mHeightScale = (float) thumbnailHeight / mImgPreview.getHeight();
startEnterAnimation();
return true;
}
});
}
}
public void startEnterAnimation() {
// NineOldAndroidを使って、古いSDKバージョンでもアニメーションできるようにしています。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mImgPreview.setPivotX(0);
mImgPreview.setPivotY(0);
mImgPreview.setScaleX(mWidthScale);
mImgPreview.setScaleY(mHeightScale);
mImgPreview.setTranslationX(mLeftDelta);
mImgPreview.setTranslationY(mTopDelta);
} else {
AnimatorProxy proxy = AnimatorProxy.wrap(mImgPreview);
proxy.setPivotX(0);
proxy.setPivotY(0);
proxy.setScaleX(mWidthScale);
proxy.setScaleY(mHeightScale);
proxy.setTranslationX(mLeftDelta);
proxy.setTranslationY(mTopDelta);
}
// アニメーションの最後を遅くするために、DecelerateInterpolatorを指定
ViewPropertyAnimator.animate(mImgPreview)
.setDuration(ANIMATION_DURATION)
.scaleX(1).scaleY(1)
.translationX(0).translationY(0)
.setInterpolator(decelerateInterpolator);
}
終了時のアニメーションを指定
finish時のアニメーションを指定します。アニメーションが終了した時にActivityをfinishするようにしています。
public void startExitAnimation() {
final boolean fadeOut;
if (getResources().getConfiguration().orientation != mOriginalOrientation) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mImgPreview.setPivotX(mImgPreview.getWidth() / 2);
mImgPreview.setPivotY(mImgPreview.getHeight() / 2);
} else {
AnimatorProxy proxy = AnimatorProxy.wrap(mImgPreview);
proxy.setPivotX(mImgPreview.getWidth() / 2);
proxy.setPivotY(mImgPreview.getHeight() / 2);
}
mLeftDelta = 0;
mTopDelta = 0;
fadeOut = true;
} else {
fadeOut = false;
}
ViewPropertyAnimator.animate(mImgPreview)
.setDuration(ANIMATION_DURATION)
.scaleX(mWidthScale).scaleY(mHeightScale)
.translationX(mLeftDelta).translationY(mTopDelta)
.setInterpolator(decelerateInterpolator)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finish();
}
});
if (fadeOut) {
ViewPropertyAnimator.animate(mImgPreview).alpha(0);
}
}
@Override
public void onBackPressed() {
// 戻るボタンでアニメーションを開始。ActionBar/ToolBarのバックボタンがある場合はそこでも呼ぶ
startExitAnimation();
}
@Override
public void finish() {
super.finish();
// Activityのデフォルトアニメーションを切る
overridePendingTransition(0, 0);
}
そもそもv21未満でも対応すべき?
GooglePlayStoreやGmailなどGoogle純正のアプリでは、v21未満非対応の動作は完全に諦めてるみたいです。Rippleエフェクトとかも全然ないですし。なので、正直対応しなくてもいいんじゃないかと思ってます。
ただAirbnbの詳細画面遷移時やFacebookの写真プレビュー遷移時のアニメーションは、やはり触ってて気持ちいいんですよね。まだAndroid4以下の方が圧倒的に多いですし、早くすべての端末がAndroid5に上がることを祈りつつ、簡単なTransitionは自前で実装しておくのがいいんじゃないかなと思いました。