OSSから学ぶActivity起動時のカッコいいアニメーション


みなさんMaterial Designやりきっていますか？
Activity起動時にReveal Effectが行われて、そのあとにふわっとコンテンツが出てくるようなUIを見たことある方も多いと思いますが、アレってめちゃくちゃカッコいいですよね。

今回はカッコいいアレが実装されている[InstaMaterial](https://github.com/frogermcs/InstaMaterial)のコードを分解しながらどういう実装になっているのかを紐解いていきたいと思います。

## 概要
今回はInstaMaterialのメインページ（MainActivity）からユーザーページ（UserProfileActivity）へ画面遷移するときのアニメーションについて分解してみました。

![InstaMaterial.gif](https://qiita-image-store.s3.amazonaws.com/0/40539/0230a278-9642-7dd3-9b63-347d41deb919.gif "InstaMaterial.gif")

_GIFアニメーションのサイズ容量を減らすためにfpsを下げているので、細かく確認したい場合はプロジェクトを各自でビルドしてください_

## MainActivityからUserProfileActivityへのIntent
MainActivityからUserProfileActivityへIntentするときのコードは以下のようになっています。

```MainActivity.java
@Override
public void onProfileClick(View v) {
    // 画面遷移後すぐに表示するReveal EffectのためにタップしたViewの位置を取得して渡している
    int[] startingLocation = new int[2];
    v.getLocationOnScreen(startingLocation);
    startingLocation[0] += v.getWidth() / 2;
    UserProfileActivity.startUserProfileFromLocation(startingLocation, this);
    
    // 画面遷移のデフォルトアニメーションを切り、Activityが切り替わっていることを感じさせないようにしている
    overridePendingTransition(0, 0);
}
```

## 画面遷移後すぐに現れるReveal Effect
UserProfileActivityのonCreateで呼んでいる`setupRevealBackground()`でReveal Effectを行う準備をしています。コードは以下のようになっています。

```UserProfileActivity.java
private void setupRevealBackground(Bundle savedInstanceState) {
    // 状態が変わるとUserProfileActivity#onStateChangeが呼ばれるようにする
    vRevealBackground.setOnStateChangeListener(this);
    
    if (savedInstanceState == null) {
        // MainActivityから渡ってきた情報を取得
        final int[] startingLocation = getIntent().getIntArrayExtra(ARG_REVEAL_START_LOCATION);
        
        // ViewTreeObserverを使うことで、vRevealBackgroundが描画されたタイミングをキャッチしてアニメーション処理を開始するようにしている
        vRevealBackground.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                vRevealBackground.getViewTreeObserver().removeOnPreDrawListener(this);
                // 実際にアニメーションを行っている部分
                // アニメーションはObjectAnimatorで行っている
                // アニメーションが終わったら状態を『FINISHED』に変更
                vRevealBackground.startFromLocation(startingLocation);
                return true;
            }
        });
    } else {
        vRevealBackground.setToFinishedFrame();
        userPhotosAdapter.setLockedAnimations(true);
    }
}
```

`vRevealBackground`は[RevealBackgroundView.java](https://github.com/frogermcs/InstaMaterial/blob/master/app/src/main/java/io/github/froger/instamaterial/ui/view/RevealBackgroundView.java)を実体化したもので、Reveal Effectの実際の処理を行ったり、状態保持を行ったりしています。
今回は割愛しますが、素敵な実装のクラスなのでそれぞれのアプリに適用する際に参考にしてみてはいかがでしょうか。

## ヘッダーとタブのアニメーション
Reveal Effectのアニメーションが終了したら状態が『FINISHED』に変更されるため、`onStateChange()`が呼び出されます。コードは以下のようになっています。

```UserProfileActivity.java
@Override
public void onStateChange(int state) {
    if (RevealBackgroundView.STATE_FINISHED == state) {
        // Reveal Effectの状態がFINISHEDに変更された場合
        
        // ヘッダーとタブがVISIBLEになる
        rvUserProfile.setVisibility(View.VISIBLE);
        tlUserProfileTabs.setVisibility(View.VISIBLE);
        vUserProfileRoot.setVisibility(View.VISIBLE);
        
        // 下部の写真グリッドの処理をはじめる
        userPhotosAdapter = new UserProfileAdapter(this);
        rvUserProfile.setAdapter(userPhotosAdapter);
        
        // タブが上から出てくるアニメーションが行われる
        animateUserProfileOptions();
        
        // ヘッダーが上から出てくるアニメーションが行われる
        animateUserProfileHeader();
    } else {
        tlUserProfileTabs.setVisibility(View.INVISIBLE);
        rvUserProfile.setVisibility(View.INVISIBLE);
        vUserProfileRoot.setVisibility(View.INVISIBLE);
    }   
}

private void animateUserProfileOptions() {
    tlUserProfileTabs.setTranslationY(-tlUserProfileTabs.getHeight());
    tlUserProfileTabs.animate().translationY(0).setDuration(300).setStartDelay(USER_OPTIONS_ANIMATION_DELAY).setInterpolator(INTERPOLATOR);
}

private void animateUserProfileHeader() {
    vUserProfileRoot.setTranslationY(-vUserProfileRoot.getHeight());
    ivUserProfilePhoto.setTranslationY(-ivUserProfilePhoto.getHeight());
    vUserDetails.setTranslationY(-vUserDetails.getHeight());
    vUserStats.setAlpha(0);

    vUserProfileRoot.animate().translationY(0).setDuration(300).setInterpolator(INTERPOLATOR);
    ivUserProfilePhoto.animate().translationY(0).setDuration(300).setStartDelay(100).setInterpolator(INTERPOLATOR);
    vUserDetails.animate().translationY(0).setDuration(300).setStartDelay(200).setInterpolator(INTERPOLATOR);
    vUserStats.animate().alpha(1).setDuration(200).setStartDelay(400).setInterpolator(INTERPOLATOR).start();
}
```

ここでの処理はヘッダーとタブのアニメーションがメインになっています。コードを読むとわかりますが、泥臭く動きを付けてカッコいいアレを作っています。
どんなカッコいいものの結局は泥臭い作業の積み重ねですよ！

## ふわっと表示される各写真
`onStateChange()`では下部の写真グリッドを表示させる処理も行われています。
この写真グリッドはシンプルなRecyclerViewになりますが、画像を読み込んだあとにスケールさせるアニメーションを入れることでこの動きを実現しています。コードは以下のようになっています。

```UserProfileAdapter.java
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    bindPhoto((PhotoViewHolder) holder, position);
}

private void bindPhoto(final PhotoViewHolder holder, int position) {
    Picasso.with(context)
            .load(photos.get(position))
            .resize(cellSize, cellSize)
            .centerCrop()
            .into(holder.ivPhoto, new Callback() {
                @Override
                public void onSuccess() {
                    // 画像の読み込みが完了したらアニメーションを行う
                    animatePhoto(holder);
                }   

                @Override
                public void onError() {

                }   
            }); 
    if (lastAnimatedItem < position) lastAnimatedItem = position;
}

private void animatePhoto(PhotoViewHolder viewHolder) {
    if (!lockedAnimations) {
        if (lastAnimatedItem == viewHolder.getPosition()) {
            setLockedAnimations(true);
        }   

        // スケールを0から1にしてふわっと表示させている
        long animationDelay = PHOTO_ANIMATION_DELAY + viewHolder.getPosition() * 30; 

        viewHolder.flRoot.setScaleY(0);
        viewHolder.flRoot.setScaleX(0);

        viewHolder.flRoot.animate()
                .scaleY(1)
                .scaleX(1)
                .setDuration(200)
                .setInterpolator(INTERPOLATOR)
                .setStartDelay(animationDelay)
                .start();
    }   
}
```

## まとめ
Activity起動時にReveal Effectが行われて、そのあとにふわっとコンテンツが出てくるようなUIの実装方法をInstaMaterialのコードを通じて紹介しました。

このUIの要であるのは間違いなくReveal Effectであると思いますが、[RevealBackgroundView.java](https://github.com/frogermcs/InstaMaterial/blob/master/app/src/main/java/io/github/froger/instamaterial/ui/view/RevealBackgroundView.java)を参考にすればアプリによって様々な形にカスタマイズできると思うのでぜひとも一度コードを読んでみてください。

また、ふわっと表示させるアニメーションには近道がないことがわかったので、良い表現になるように地道にアニメーションの調整を行いカッコよくなるまで泥臭く頑張りましょう。