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


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

![ezgif.com-resize.gif](https://qiita-image-store.s3.amazonaws.com/0/15267/459fe824-d5e3-6d3a-384a-41509f9c0f6f.gif "ezgif.com-resize.gif")


実装方法については、[Android　Activity Transitions を実装する](http://y-anz-m.blogspot.jp/2014/12/androidactivity-transitions.html)がわかりやすいです。

ただ、これは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](https://qiita-image-store.s3.amazonaws.com/0/15267/fee601ca-df67-4d0e-df9c-c64a45798ffd.gif "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は自前で実装しておくのがいいんじゃないかなと思いました。