0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

3Dに縦回転するアニメーションを実装した話

Last updated at Posted at 2022-08-11

現在作成している自作アプリに、3D方向にviewが回転するアニメーションを取り入れたかったので作成したときの備忘録です。

やりたかったこと

  • viewをタップすると、メンコみたいに縦方向に3D回転にする
  • 表面と裏面があり、それぞれでレイアウトが違う
  • タップするたびに表面⇄裏面で交互に表示される
  • RecyclerViewに表示するViewとして扱いたい

やったこと

レイアウト

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:id="@+id/card"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_marginBottom="15dp"
        android:gravity="center_horizontal">

        <com.google.android.material.card.MaterialCardView
            android:id="@+id/front"
            android:layout_width="match_parent"
            android:layout_height="120dp"
            style="@style/Widget.Material3.App.CardView.Outlined"
            app:cardBackgroundColor="#ffc0cb">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="32sp"
                    android:text="表面"/>
            </LinearLayout>
        </com.google.android.material.card.MaterialCardView>
        
        <com.google.android.material.card.MaterialCardView
            android:id="@+id/back"
            android:layout_width="match_parent"
            android:layout_height="120dp"
            style="@style/Widget.Material3.App.CardView.Outlined"
            android:visibility="gone"
            tools:visibility="visible"
            app:cardBackgroundColor="#e6e6fa">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="32sp"
                    android:text="裏面"/>
            </LinearLayout>
        </com.google.android.material.card.MaterialCardView>
    </LinearLayout>
</FrameLayout>

cardという親viewの下に子viewとして、表面(front)と裏面(back)のレイアウトを定義しておき、片方(裏面)をデフォルトで非表示としておきます。

ロジック

ObjectAnimatorを利用して、3D回転させるようにしました。
https://developer.android.com/guide/topics/graphics/prop-animation?hl=ja#object-animator

fun reverse(card: View, front: View, back: View) {
    card.apply {
        pivotY = height.toFloat()
    }
    val centerY = card.height / 2.0F

    val yFront = ObjectAnimator.ofFloat(card, "y", 0.0f, -centerY).apply {
        duration = 150L
        interpolator = LinearInterpolator()
    }

    val rotateXFront = ObjectAnimator.ofFloat(card, "rotationX", 0.0f, 90.0f).apply {
        duration = 150L
        interpolator = LinearInterpolator()
    }

    val yBack = ObjectAnimator.ofFloat(card, "y", centerY, 0.0f).apply {
        duration = 150L
        interpolator = LinearInterpolator()
    }

    val rotateXBack = ObjectAnimator.ofFloat(card, "rotationX", 270.0f, 360.0f).apply {
        duration = 150L
        interpolator = LinearInterpolator()
    }

    AnimatorSet().apply {
        playTogether(yFront, rotateXFront)
        doOnEnd {
            // 表面と裏面の表示/非表示をチェンジする
            front.isVisible = false
            back.isVisible = true
            card.pivotY = 0.0f
            
            AnimatorSet().apply {
                playTogether(yBack, rotateXBack)
                doOnEnd {
                    card.resetPivot()
                }
                start()
            }
        }
        start()
    }
}

共通化とかしていない汚いコードで大変恐縮です🙇‍♂️ が、流れとしては以下の通りです。

  1. 表面となるview(front)と裏面となるview(back)、およびそれらを内包しているview(card)を渡します
  2. cardのY方向のピボット位置をbottomとします。
  3. cardのY方向の中心位置を取得しておきます
  4. 表面表示時のアニメーションを定義
    1. 150msかけてYの中心位置まで移動
    2. 同時間かけて縦方向に0°->90°転回
  5. 裏面表示時のアニメーションを定義
    1. 150msかけてYのtopまで移動
    2. 同時間かけて縦方向に270°->360°転回
  6. AnimatorSet#playTogetherにより、表面の移動処理と回転処理を同時に処理するようにする
  7. 表面の処理が済んだ時点(このとき、View自体は90°転回している状態)で、表面と裏面の表示/非表示をチェンジします。
  8. 合わせて、cardのY方向のピボット位置をtopに変更します。

最後に、これを呼び出す処理を記載すれば完了です。

binding.front.setOnClickListener(view -> {
    reverse(binding.card, view, binding.back);
});
binding.back.setOnClickListener(view -> {
    reverse(binding.card, view, binding.front);
});

タップできる面が表面になっているので、表面がview, 裏面が対になるview(front/back)として先ほどのメソッドを呼び出します。

実演

こんな感じで動作します。(gifの都合でカクついていますが、実際は滑らかです)

補足

ピボットの設定とy方向の移動が必要な理由
これらを行わない場合、viewが見切れてしまいました。

上が元々の状態で下が45°まで回転させたときの様子ですが、横が見切れていることがわかります。

rotationXのデフォルトのピボットはviewの中心であり、そこを中心としてviewが拡縮されるようなので、
Yのピボットをviewのbottomとした上で、y方向の移動を併せて行なっています。これにより、viewの中心で回転しているように見せつつ、viewの横が見切れるのを抑えています。

反転したときはviewのtopをピボットとします

裏面の回転が270°->360°の理由

実際にやってみるとわかりますが、90°->180°にすると上下が反転して表示されてしまいました。

270°->360°にすることで、「元々反転していたものを反転(=元に戻す)」という動作となるため、この問題を解決できます。

今回は以上です。
コメント等お待ちしてます。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?