Help us understand the problem. What is going on with this article?

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした