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

Circular RevealとPaletteによるそれっぽい画面遷移アニメーション

みなさん、アニメーション付けてますか?

画面遷移の際に用いられるアニメーションにShared Elementというものがあります。
一覧画面から詳細画面に遷移する際には主にこちらが用いられますが、私のケースでは一覧のサムネイル画像と詳細で表示する画像に差異があるという要件があったのでShared Elementは不適当でした。

そのため代わりとなる遷移アニメーションはないかと考え、今回の実装を試してみました。

通常の画面遷移

まずは何の飾り気もない画面遷移を実装してみます。
分かりやすくするために、デフォルトで設定されているActivityの遷移アニメーションは削除しています。

activity_main.xml
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/main_image"
        android:layout_width="128dp"
        android:layout_height="128dp"
        android:scaleType="centerCrop"
        android:src="@drawable/image" />

</FrameLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<View>(R.id.main_image).setOnClickListener {
            startActivity(Intent(this, SubActivity::class.java))
        }
    }
}
activity_sub.xml
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/sub_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:src="@drawable/image" />

</FrameLayout>
SubActivity.kt
class SubActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sub)
        // 遷移アニメーションを削除
        overridePendingTransition(0, 0)
    }
}


画面がどう遷移しているのか分かりにくいですし、目がチカチカしてユーザに優しくありませんね。

Circular Revealによる画面遷移

次にCircular Revealによるアニメーションを設定していきます。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<ImageView>(R.id.main_image).setOnClickListener { view ->
            Intent(this, SubActivity::class.java)
                    // 遷移元のViewの中心座標を渡す
                    .putExtra("posX", view.x + (view.width / 2))
                    .putExtra("posY", view.y + (view.height / 2))
                    .let { intent -> startActivity(intent) }
        }
    }
}
SubActivity.kt
class SubActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sub)
        // 遷移アニメーションを削除
        overridePendingTransition(0, 0)

        startAnimation()
    }

    private fun startAnimation() {
        // 遷移元のViewの座標を受け取る
        val posX = intent.getFloatExtra("posX", 0F)
        val posY = intent.getFloatExtra("posY", 0F)

        // Viewの準備ができてからアニメーションを実行する
        findViewById<View>(android.R.id.content).doOnPreDraw { view ->
            ViewAnimationUtils.createCircularReveal(
                    view /* 対象のView */,
                    posX.toInt() /* 中心のX座標 */,
                    posY.toInt() /* 中心のY座標 */,
                    0F /* 開始時の半径 */,
                    Math.max(view.width, view.height).toFloat() * 2 /* 終了時の半径 */
            )
                    // アニメーションにかける時間
                    .apply { duration = 1000 }
                    .start()
        }
    }
}

ポイントとなるのは下記の点です。

・Viewがレイアウトにバインドされてからアニメーションを実行する
Kotlinの拡張関数であるdoOnPreDrawを使用することで、シンプルに記述しています。

・遷移元を基準にアニメーションの開始位置を設定する
今回は遷移元のViewの中心を開始位置としていますが、タップした位置を開始位置にしてみたりするとよりいい感じになるかもしれません。

・終了時の半径は大きめに設定する
ここの値が小さいと全体を覆うことができません。今回は雑に長辺の2倍としていますが、ちゃんとやるのであれば公式等を用いて算出するのがよさそうです。

注意点として、今回はアニメーションの挙動を分かりやすくするために時間を1sに設定してあります。
Material Designのガイドライン的には300msほどを推奨しているようですので、参考にするとよいでしょう。
https://material.io/design/motion/speed.html#duration

Animated elements that traverse a large portion of the screen have the longest durations.


アニメーションを設定することによって、どこからどこに遷移したのか?というのが分かりやすくなりました。

今回は遷移後の画面で画像が一瞬で表示されていますが、ネットワークから取得する場合など表示まで時間がかかることもあると思います。
そういった場合にどのようになるかを見てみましょう。

悪くはない気もしますが、最初に固定色で覆われてから画像が表示されるのでちょっと変化が激しく感じますね。

Circular Reveal + Paletteによる画面遷移

変化の度合いを少しでも緩和するために、遷移元の画像の色を用いて遷移先の画像が表示されるまでの間を補完することとします。
この画像から色を取得する処理にPalette APIを利用します。

app/build.gradle
dependencies {

    // Palette
    implementation "androidx.palette:palette:${version}"

}
MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<ImageView>(R.id.main_image).let { imageView ->
            imageView.setOnClickListener { view ->
                imageView.drawable?.toBitmap()?.let { bitmap ->
                    Palette.from(bitmap).generate { palette ->
                        Intent(this, SubActivity::class.java)
                            // 遷移元のViewの中心座標を渡す
                            .putExtra("posX", view.x + (view.width / 2))
                            .putExtra("posY", view.y + (view.height / 2))
                            // 画像の色を渡す
                            .putExtra("color", palette?.getDominantColor(Color.TRANSPARENT))
                            .let { intent -> startActivity(intent) }
                    }
                }
            }
        }
    }
}
SubActivity.kt
class SubActivity : AppCompatActivity() {

    ~~~

    private fun startAnimation() {
        ~~~
        // 遷移元の色を受け取る
        val color = intent.getIntExtra("color", Color.BLACK)
        findViewById<ImageView>(R.id.sub_image).setBackgroundColor(color)
        ~~~
    }
}

ポイントとなるのは下記の点です。

・画像から色を取得する
Paletteクラスでは様々な種類の色を取得することができますが、今回はgetDominantColorを使用します。これは画像の主成分となる色を返してくれます。

・色を遷移後のViewの背景色に指定する
こうすることで、画像が読み込まれるまでの表示とすることができます。


ちょっと分かりにくいですが、水の部分の色が広がっていますね。
もう少し分かりやすいものだと下記のようになります。

それっぽい!

所感

遷移元と遷移先の種類によって、遷移アニメーションには様々な選択があります。
Circular RevealやPaletteなどでちょっとしたアクセントを付けるだけでも効果はあります。
劇的な変化が望めるようなものではないかもしれませんが、少しでもユーザが使いやすいアプリを追求していきたいものですね!

最後に、アニメーション時間を推奨の300msにしたもので締めとさせていただきます。

それっぽい!

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
ユーザーは見つかりませんでした