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

MotionLayoutのサンプルコードから学ぶMotion Editor

この記事はコネヒト Advent Calendar 2019 3日目の記事になります。

はじめに

Android Studio 4.0 Canary 4に新機能として導入されたMotion Editorについて調べてみたので、公開されているMotionLayoutのサンプルコードをベースに、どのように利用されているのか紹介します。

MotionLayoutとは?

  • ConstraintLayout 2.0に含まれている機能
  • 始まりと終わりのレイアウトをXMLで定義することによって簡単にアニメーションを実装することができる

Motion Editorとは?

  • Android Studio 4.0 CanaryからMotion Editor導入された
  • IDE内でViewの関係性やアニメーションを確認、設定できる

サンプルコードの紹介

今回利用したサンプルコードはPiotrPrusさんのMotionLayoutPlaygroundリポジトリです。こちらのリポジトリでは、いくつかMotionLayoutのサンプルがありますが、「scene13(RV with ML)」のTinder風アニメーションのMotionLayoutについて紹介します。まずは以下のキャプチャを見て挙動を確認しましょう。

motionlayout.gif

グリッドリスト上に複数のCardViewがあり、そのCardViewを左にスワイプする(like)と、リストから削除されます。反対に右にスワイプする(dislike)と先ほどと同様にリストから削除されるようにMotionLayoutで実装されています。

サンプルコードの解説

対象画面のコード一式は下記の通りです。

item_scene_13_grid.xmlの中身を見てみると、Viewの要素にCardViewとそれに対応したlike、dislike用のオーバーレイが配置されています。

item_scene_13_grid.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:clipChildren="false"
    android:clipToPadding="false"
    app:layoutDescription="@xml/ml_scene_13">

    <!-- CardView -->
    <com.google.android.material.card.MaterialCardView
        android:id="@+id/cardViewScene13"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorGrey"
        app:cardBackgroundColor="@color/colorBlue"
        app:cardCornerRadius="10dp"
        app:cardElevation="4dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/item_number"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:textAppearance="?attr/textAppearanceListItem"
                tools:text="1" />

            <TextView
                android:id="@+id/content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:textAppearance="?attr/textAppearanceListItem"
                tools:text="test" />
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>

    <!-- Like用のオーバーレイ -->
    <View
        android:id="@+id/cardScene13LikeOverlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/frame_overlay_like"
        app:layout_constraintBottom_toBottomOf="@id/cardViewScene13"
        app:layout_constraintEnd_toEndOf="@id/cardViewScene13"
        app:layout_constraintStart_toStartOf="@id/cardViewScene13"
        app:layout_constraintTop_toTopOf="@id/cardViewScene13" />

    <!-- Dislike用のオーバーレイ -->
    <View
        android:id="@+id/cardScene13DislikeOverlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/frame_overlay_dislike"
        app:layout_constraintBottom_toBottomOf="@id/cardViewScene13"
        app:layout_constraintEnd_toEndOf="@id/cardViewScene13"
        app:layout_constraintStart_toStartOf="@id/cardViewScene13"
        app:layout_constraintTop_toTopOf="@id/cardViewScene13" />
</androidx.constraintlayout.motion.widget.MotionLayout>

Motion Editorで処理の流れを見る

では、MotionLayoutの構成を見てみましょう。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f32333339312f37323063313430372d636338312d336136322d363431312d3437613231666330613363662e706e67.png

グリッドリスト部分に当たる、item_scene_13_grid.xmlをMotion Editorを開いたキャプチャです。これまではXMLで書いていたものが、GUIで表示、設定できるようになりました!Motion Editorを見てみると、5つのConstraintSetで構成されています。

  • rest : 初期状態
  • like : いいね
  • goneRight : 右に消える
  • dislike : 好まない
  • goneLeft : 左に消える

ConstraintSetとは、Viewの状態を定義でき、例えば初めのViewの状態と終わりのViewの状態やアニメーションを定義することができます。これら複数のConstraintSetをどのように組み合わせてlike、dislikeの処理を行なっているのでしょうか?それぞれ確認してみたい思います。

like処理

ConstraintSet rest

CardViewを表示する部分に当たるConstraintSet restを選択すると、ConstraintSetの中に3つのConstraintが定義されていて、cardViewScene13、cardScene13LikeOverlay、cardScene13DislikeOverlayのConstraintがあります。さらにConstraint cardViewScene13を選択すると、それぞれプロパティが設定されています。

スクリーンショット 2019-12-01 20.47.37.png

ConstraintSet restを矢印がlikeとdislikeのConstraintSetに設定されています。まずはlikeの矢印を見てみますが、矢印はTransitionを表します。Transitionを再生すると右に回転していくことがわかります。

スクリーンショット 2019-12-01 19.30.16.png

ちなみにrestからlikeのTransition上のマークはスワイプやクリックイベントを表すマークとなっています。
スクリーンショット 2019-12-03 8.38.23.png

ConstraintSet like

次にいいねの処理に当たる、ConstraintSet likeについて見てみます。CardViewが回転されている状態であることがわかります。

スクリーンショット 2019-12-01 19.43.39.png

Constraint cardViewScene13で右にローテーションしていることがわかります。
スクリーンショット 2019-12-01 21.28.03.png

次にTransitionがlikeからgoneRightへと定義されており、Transitionを再生してみるとViewが画面外に移動し見えなくなることがわかります。autoTransitionプロパティにanimateToEndが設定されていますが、これによってlikeからgoneRight状態に自動的にアニメーション化されます。
スクリーンショット 2019-12-01 19.47.06.png

ConstraintSet goneRight

ここまでrestからlike、goneRightまでConstraintSetを組み合わせてアニメーションを実装していることがわかりました。ConstraintSet goneRightの中にはConstraintとして、cardViewScene13などが定義されており、左のプレビューからcardViewScene13が画面でかつalpha0になっていることがわかります。このようにして右スワイプしてCardViewが消えるアニメーションを実装されています。

スクリーンショット 2019-12-01 21.33.59.png

以上がlike処理になります。restなどのConstraintSet以下に破線がありますが、これはConstraintSetのderiveConstraintsFromプロパティを利用すると表示されます。likeの場合はrestから派生しているという意味になります。リストから削除されている部分については、コード側で実装されているため別途後述します。次にdislike処理についても確認します。

dislike処理

ConstraintSet dislike

likeと同様にConstraintSetでrestからdislikeが定義されています。左スワイプをされるとローテートされた状態がConstraintSet dislikeです。さらにConstraintSet dislikeのcardViewScene13を見てみると左向きにローテーションされているのがわかります。

スクリーンショット 2019-12-01 22.59.00.png

次にTransitionを見てみるとConstraintSet dislikeからConstraintSet goneLeftに設定されているのがわかります。
スクリーンショット 2019-12-01 23.04.23.png

ConstraintSet goneLeft

rest -> dislike -> goneLeft ConstraintSetとTransitionが定義されており、goneLeftでは、alpha:0で透過し、rotation:-70、layout_marginEnd:600dpで画面外にViewが移動していることがわかります。ここまでがdislikeのアニメーションになります。

スクリーンショット 2019-12-01 23.06.17.png

like、dislike後のリスト削除

likeとdislike後にリストから削除されるようになっていて、ここの部分はコード側で実装されているため解説します。
Scene13Fragmentでは、MySceneGridItemRecyclerViewAdapterを使っています。リスト削除についてですが、MotionLayout.TransitionListenerインターフェースが提供されていて、goneRight、goneLeftのトランジッションが完了したタイミングでリストから削除しています。

MySceneGridItemRecyclerViewAdapter.kt
class MySceneGridItemRecyclerViewAdapter(
    private val mValues: MutableList<DummyItem>,
    private val animationListener: (state: AnimationState) -> Unit
) : RecyclerView.Adapter<MySceneGridItemRecyclerViewAdapter.ViewHolder>() {
    // 〜省略〜

    private val removeItemListener: (Int) -> Unit = {
        Handler().postDelayed({
            mValues.removeAt(it)
            notifyDataSetChanged()
        }, 100)
    }

    inner class ViewHolder(val mView: View, removeItemListener: (Int) -> Unit) :
        RecyclerView.ViewHolder(mView) {
        val mIdView: TextView = mView.item_number
        val mContentView: TextView = mView.content
        internal var position = 0

        override fun toString(): String {
            return super.toString() + " '" + mContentView.text + "'"
        }

        init {
            (mView as MotionLayout).setTransitionListener(object : MotionLayout.TransitionListener {
                override fun onTransitionTrigger(
                    p0: MotionLayout?,
                    p1: Int,
                    p2: Boolean,
                    p3: Float
                ) {
                }

                override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
                    when {
                        p1 == R.id.rest && p3 > 0f -> animationListener(AnimationState.STARTED)
                    }
                }

                override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
                    animationListener(AnimationState.COMPLETED)
                    if (p1 == R.id.goneRight || p1 == R.id.goneLeft) {
                        Log.d("Adapter", "Remove item at position: $position")
                        removeItemListener(position)
                    }
                }

                override fun onTransitionStarted(p0: MotionLayout?, startId: Int, endId: Int) {

                }
            })
        }
    }
}

おわりに

簡単にでしたが、MotionLayoutのサンプルを用いてMotion Editorについて触れてみました。GUIによってConstraintSetとTransitionの関係性がわかりやすくなり、Transitionも手軽に再生できるのでとても便利です。Motion EditorはAndroid Studio 4.0 Canaryの新機能でもっと触っておかしいところがあればIssueを投げていけるように頑張ります。

PR

コネヒトではエンジニアを募集しています!家族向けサービスをつくりたいエンジニアの皆さん、お待ちしています。
Connehito Image
家族の課題を解決するサービスのMAUを増やすAndroidエンジニア募集!

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