みなさん、アニメーション付けてますか?
画面遷移の際に用いられるアニメーションにShared Elementというものがあります。
一覧画面から詳細画面に遷移する際には主にこちらが用いられますが、私のケースでは一覧のサムネイル画像と詳細で表示する画像に差異があるという要件があったのでShared Elementは不適当でした。
そのため代わりとなる遷移アニメーションはないかと考え、今回の実装を試してみました。
通常の画面遷移
まずは何の飾り気もない画面遷移を実装してみます。
分かりやすくするために、デフォルトで設定されているActivityの遷移アニメーションは削除しています。
<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>
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))
}
}
}
<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>
class SubActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sub)
// 遷移アニメーションを削除
overridePendingTransition(0, 0)
}
}
Circular Revealによる画面遷移
次にCircular Revealによるアニメーションを設定していきます。
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) }
}
}
}
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を利用します。
dependencies {
// Palette
implementation "androidx.palette:palette:${version}"
}
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) }
}
}
}
}
}
}
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などでちょっとしたアクセントを付けるだけでも効果はあります。
劇的な変化が望めるようなものではないかもしれませんが、少しでもユーザが使いやすいアプリを追求していきたいものですね!