LoginSignup
16
10

More than 3 years have passed since last update.

ConstraintLayout に新しく追加された Carousel を使ってみた

Last updated at Posted at 2020-11-20

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 とモデルのマッピングや、細かな表示制御のロジックを提供してくれている感じでした。

リンク

16
10
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
16
10