MotionLayoutを使って無限Pagerを実装する

この記事は、[はてなエンジニア Advent Calendar 2019](https://qiita.com/advent-calendar/2019/hatena) の 23 日目の記事です。

MotionLayout でこのような無限 Pager を実装してみたというちょっとしたお遊びです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/c46c7ccb-8236-eff8-d81d-adfa070ee2a7.gif" width="40%">

## MotionLayout で Pager を作る考え方

- 表示させているもの + 前後に表示されず隠れている View を用意する  
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/c003e757-bd58-b6ea-10ca-874ba4080e3f.png" width="45%">

- スワイプさせて次の View が表示されたタイミングで、表示をそのままに Motion を最初の状態に戻す  
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/80395d38-a1fe-2271-29ec-dafcc940d776.png" width="45%">


といった感じで Pager を実現させています。  



## 実装  

### MotionScene の定義

`ConstraintSet` を最初の状態、右にスワイプした時、左にスワイプした時の 3 パターン用意します。
最初の状態を起点に `Transition` を右にスワイプした時、左にスワイプした時で 2 つ用意します。　　

```
<?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">

    <ConstraintSet android:id="@+id/base_state">
        <Constraint android:id="@id/centerView">
            <Layout
                android:layout_width="320dp"
                android:layout_height="320dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />

        </Constraint>

        <Constraint android:id="@id/leftView">
            <Layout
                android:layout_width="320dp"
                android:layout_height="320dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />
        </Constraint>

        <Constraint android:id="@id/rightView">
            <Layout
                android:layout_width="320dp"
                android:layout_height="320dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toEndOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/move_left_to_right">
        <!-- 省略 -->
        <!-- 左へスワイプした時のViewの状態を定義する -->
    </ConstraintSet>

    <ConstraintSet android:id="@+id/move_right_to_left">
        <!-- 省略 -->
        <!-- 右へスワイプした時のViewの状態を定義する -->
    </ConstraintSet>

    <Transition
        motion:constraintSetEnd="@id/move_left_to_right"
        motion:constraintSetStart="@id/base_state">
        <OnSwipe
            motion:dragDirection="dragRight"
            motion:onTouchUp="autoCompleteToStart"
            motion:touchAnchorId="@id/centerView"
            motion:touchAnchorSide="right" />
    </Transition>

    <Transition
        motion:constraintSetEnd="@id/move_right_to_left"
        motion:constraintSetStart="@id/base_state">
        <OnSwipe
            motion:dragDirection="dragLeft"
            motion:onTouchUp="autoCompleteToStart"
            motion:touchAnchorId="@id/centerView"
            motion:touchAnchorSide="left" />
    </Transition>

</MotionScene>
```

### アイテムの表示処理の実装  

アニメーションが完了したら(スワイプで完全に移動した時)、リストの位置をずらしてアニメーションを最初に戻す (`motionLayout?.progress = 0F`) だけです。  

```
motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
    override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
        when (currentId) {
            R.id.move_left_to_right -> {
                if (currentPosition > 0) {
                    currentPosition--
                } else {
                    currentPosition = itemList.lastIndex
                }
                motionLayout?.progress = 0F
                updateView()
            }
            R.id.move_right_to_left -> {
                if (currentPosition < itemList.lastIndex) {
                    currentPosition++
                } else {
                    currentPosition = 0
                }
                motionLayout?.progress = 0F
                updateView()
            }
        }
    }
})
```


MotionLayout を使っているため `KeyFrameSet` を調整することで様々なアニメーションを実現できます。  


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/2069b44b-7725-7cf9-eff7-265f1263d1ce.gif" width="40%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/baf544f1-0184-de11-46c1-48275ceb9724.gif" width="40%">


全コードはこちらに公開しています
https://github.com/NUmeroAndDev/MotionLayoutPager-android



MotionLayout で無限 Pager を実装してみました。 
プロダクトに当てはめるには考慮すべきことや表現できないパターンがありそうですが、ぬるぬるアニメーションさせるなら MotionLayout は便利だなと思いました。
来年には MotionEditor が使えるようになりそうなので、MotionLayout をもっと使っていきたいですね :dancer: 

明日のはてなエンジニア Advent Calendar 2019 は @hitode909 さんです！