Android

v21未満の端末で、AirbnbアプリのようなShared Elementっぽい動きを実装する

More than 3 years have passed since last update.


SharedElementとは

sharedElementを指定してActivityを起動すると、遷移前の画面の一部が拡大して遷移後の画面の一部になるようなtransitionが実現できます。Airbnbで詳細画面に遷移する時の写真の動きみたいなやつです。

ezgif.com-resize.gif

実装方法については、Android Activity Transitions を実装するがわかりやすいです。

ただ、これはAPIv21以降じゃないと使えません。実装に使うActivityOptionsCompat.makeSceneTransitionAnimationのコードを見ると、完全にv21未満は対象外って感じになってます。


ActivityOptionsCompat.java

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が表示されるだけのサンプルです。

ezgif.com-crop (3).gif

以下、簡単に実装の説明です。


Activity起動時にtransition対象のViewの情報をintentにセット

アニメーションする対象のViewを渡して、Top、Leftの座標やWidth、Heightを渡します。


DetailActivity.java

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を開始することができ、アニメーションをスムーズに表示することができます。


DetailActivity.java

@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するようにしています。


DetailActivity.java

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は自前で実装しておくのがいいんじゃないかなと思いました。