- 2020/12/18
ConstraintLayout 2.1.0-alpha2 で追加された無限ループについて更新
ConstraintLayout 2.1.0-alpha1 に Carousel というものが導入されたので、何ができるのかとどのように使うのかを書いていきます。
(Carousel は alpha1 で入ったばかりの仕組みなので、大きく変更されることがあり得ます)
Carousel の仕組み
過去に似たような実装をしたときに書いていますが、ベースのなる状態と左右に動かした状態の 3 パターンを用意しておき、アニメーションでスワイプの動きをして中身を動かしているように見せるという方法になっています。
基本的な実装
仕組みでもあるとおり、ベースとなる状態と左右に動かした時の 3 パターンの状態を定義し、それらをアニメーションさせる感じになります。
まずは MotionLayout で使う View の定義をします。
コード
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/carousel_scene">
<TextView
android:id="@+id/textView0"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="16dp"
android:text="textView0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/textView1"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView1"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="16dp"
android:text="textView1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/textView2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="150dp"
android:layout_height="150dp"
android:text="textView2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView3"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:text="textView3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView4"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:text="textView4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView3"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="100dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="100dp" />
</androidx.constraintlayout.motion.widget.MotionLayout>
今回は 4 つの TextView を並べています。
4 つの理由は今回のレイアウトの場合は 4 つまで Carousel のアイテムが一度に表示されるためです。
次に MotionScene の定義をします。
コード
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
android:id="@+id/forward"
motion:constraintSetEnd="@+id/next"
motion:constraintSetStart="@id/base_state">
<OnSwipe
motion:dragDirection="dragLeft"
motion:touchAnchorSide="left" />
</Transition>
<Transition
android:id="@+id/backward"
motion:constraintSetEnd="@+id/previous"
motion:constraintSetStart="@+id/base_state">
<OnSwipe
motion:dragDirection="dragRight"
motion:touchAnchorSide="right" />
</Transition>
<!-- ベースとなるViewの状態を定義する -->
<ConstraintSet android:id="@+id/base_state">
<Constraint
android:id="@+id/textView1"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@id/guideline"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintDimensionRatio="w,1:1"
motion:layout_constraintEnd_toEndOf="@id/guideline2"
motion:layout_constraintHorizontal_bias="0.5"
motion:layout_constraintStart_toStartOf="@id/guideline"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/textView3"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@id/guideline2"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<!-- 左へスワイプした時のViewの状態を定義する -->
<ConstraintSet android:id="@+id/next">
<Constraint
android:id="@+id/textView2"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@id/guideline"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintDimensionRatio="1:1"
motion:layout_constraintEnd_toStartOf="@id/guideline2"
motion:layout_constraintStart_toEndOf="@id/guideline"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/textView4"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@id/guideline2"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<!-- 右へスワイプした時のViewの状態を定義する -->
<ConstraintSet android:id="@+id/previous">
<Constraint
android:id="@+id/textView2"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="@id/guideline2"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/textView3"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/textView2"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/textView1"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintDimensionRatio="1:1"
motion:layout_constraintEnd_toStartOf="@id/guideline2"
motion:layout_constraintStart_toEndOf="@id/guideline"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/textView0"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@id/guideline"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</MotionScene>
少し長いですが、3 パターンの View の状態を定義して、それらをアニメーションできるように Transition
を設定しています。
ここまでが準備で、ようやく本題の Carousel
の実装になります。
MotionLayout
のなかに以下を追加します。
<androidx.constraintlayout.helper.widget.Carousel
android:id="@+id/carousel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:carousel_backwardTransition="@+id/backward"
app:carousel_forwardTransition="@+id/forward"
app:carousel_nextState="@+id/next"
app:carousel_previousState="@+id/previous"
app:carousel_firstView="@+id/textView2"
app:constraint_referenced_ids="textView0,textView1,textView2,textView3,textView4" />
ここでは Carousel
で使用するパラメータの設定をしています。
-
carousel_backwardTransition
/carousel_forwardTransition
MotionScene
で設定したTransition
を設定し、それを使ってアニメーションさせます。 -
carousel_nextState
/carousel_previousState
MotionScene
で設定したConstraintSet
を設定します。 -
carousel_firstView
Carousel
で真ん中に表示される View を指定します。
Carousel
の定義が終わりましたが、これだけではなくコードでの設定も必要になります。
val carousel = findViewById<Carousel>(R.id.carousel)
carousel.setAdapter(object : Carousel.Adapter {
override fun count(): Int = list.size
override fun populate(view: View, index: Int) {
if (view !is TextView) return
val item = list[index]
view.text = item.text
}
override fun onNewItem(index: Int) {
}
})
Carousel.Adapter
というものがあり、これを使って Carousel の表示処理を行います。

Carousel のアイテムを追加・削除する
val carousel = findViewById<Carousel>(R.id.carousel)
mutableList.add(Hoge())
carousel.refresh()
mutableList.clear()
carousel.refresh()
Carousel.Adapter
で使用しているリストを操作した後に Carousel#refresh
を呼ぶと Carousel が更新されます。
また、
app:carousel_emptyViewsBehavior="invisible/gone"
を Carousel
に定義することでアイテムが空の時の Carousel
の表示状態を指定することができます。
無限 Carousel の実装(12/18 追記)
ConstraintLayout 2.1.0-alpha2 で Carousel に無限ループのオプションが追加されました。
Carousel の helper に app:carousel_infinite="true"
を追加するだけで無限ループが実現できます。

Carousel の仕組み全体を提供するという感じではなく、 シンプルに MotionLayout
を使っただけではできなかった Carousel とモデルのマッピングや、細かな表示制御のロジックを提供してくれている感じでした。