ViewPager/ViewPager2のPageTransformer実装カタログ

ViewPagerおよびViewPager2ではPageTransformerで比較的シンプルな実装でページ遷移のアニメーションをカスタマイズすることができます。遷移アニメーションとその実装方法を紹介していきます。

||||
|:-:|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/f8c9fd81-b250-718e-5773-7dad39970658.gif)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/09f75846-0d30-2e7c-5036-0b91d72b9462.gif)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/2d6ca9bf-1c5d-1b92-8f00-089b88dcf401.gif)|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/0bbe10b2-97ba-7139-72fc-508c00142a68.gif)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d7a8712c-814c-3b95-cb1f-f13b5444c093.gif)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/e8e95833-1b6f-e6d9-a3cb-8e6d92d9092c.gif)|

# PagerTransformerとは

ViewPager.PageTransformerの定義は以下

```java:ViewPager.java
public interface PageTransformer {
    void transformPage(@NonNull View page, float position);
}
```

ViewPager2.PageTransformerの定義は以下

```java:ViewPager2.java
public interface PageTransformer {
    void transformPage(@NonNull View page, float position);
}
```

**完全に一致！** まあ意図して同じにしているのでしょうけど

第一引数のpageはそれぞれのページのルートViewが渡され、positionは現在のページを0として1ページ右が1、1ページ左が-1となる値が渡されます。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/6c76553a-0742-eb5b-b4e1-5531b6f70fa5.png)
この位置情報を元にViewに操作を行うことでアニメーションを実装します。
transformPageは現在のページと左右のページに対して呼び出されますが、setOffscreenPageLimitで保持するページ数を変更することで、さらに広い範囲のページも表示させることもできます。

transformPageにて各ページのViewの位置や大きさなどを操作することになりますが、デフォルト動作に対する差分操作を行うことになりますので、例えばスクロールさせずに位置を固定させたい場合は、`page.translationX = -it.width * position`の用にデフォルトのスクロールによる移動分を打ち消すような操作を実装します。
positionの小数部が0の状態はアニメーションが終了している状態ですので、この状態では中央以外のページが表示されたり、中央のページの状態が変化の途中になったりしないように実装します。

設定メソッドにはちょっと違いがあります。

```java:ViewPager.java
public void setPageTransformer(boolean reverseDrawingOrder,
        @Nullable PageTransformer transformer)
```

```java:ViewPager2.java
public void setPageTransformer(@Nullable PageTransformer transformer)
```

ViewPagerはreverseDrawingOrderを指定します、これは各ページのレイヤー順序を変更できます。
falseではページ番号の大きい方が上に描画されますが、trueをしているすると逆にページ番号の若い方を上に表示されるようになります。

|reverseDrawingOrder=false|reverseDrawingOrder=true|
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d23b14b0-ec6d-0068-d054-b76ec985b8cd.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/a576d30c-9442-1b9a-3285-59ca25e50aea.png)|

ViewPager2ではこのパラメータはなく、falseと同様にページ番号が若い方が上に表示されます。レイヤーの順序を変更したい場合はtranslationZで変更します。現在ではminSdkVersionが21未満であることは珍しいと思うので、ViewPagerもViewPager2もtranslationZでレイヤー順を指定するようにするのが良いかと思います。

# 実装カタログ

## ズームアウト

ViewPagerの説明ページに載っているやつです。ちょっとパラメータの扱いを変更しています。

|translationXで隙間を埋める場合|translationXの補正なし|
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/09f75846-0d30-2e7c-5036-0b91d72b9462.gif)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/09d65058-9ee3-d0d2-13fb-e0b67f59e0b9.gif)|

実装は以下になります。移動に応じてページサイズとアルファを変更しています。

```kotlin
binding.viewPager.setPageTransformer { page, position ->
    page.also {
        if (abs(position) >= 1f) {
            it.alpha = 0f
            return@setPageTransformer
        }
        val scale = (1 - abs(position) / 2).coerceAtLeast(MIN_SCALE)
        it.scaleX = scale
        it.scaleY = scale
        it.alpha = (1 - abs(position)).coerceAtLeast(MIN_ALPHA)
        it.translationX = (1 - scale) * it.width / 2 * if (position > 0) -1 else 1
    }
}

private const val MIN_SCALE = 0.85f
private const val MIN_ALPHA = 0.5f
```

## 奥行き

これもViewPagerの説明ページに載っているやつです。同様にちょっとパラメータの扱いを変更しています。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/2d6ca9bf-1c5d-1b92-8f00-089b88dcf401.gif)

実装は以下になります。左側のページを下に表示するためtranslationZを操作しています。

```kotlin
binding.viewPager.setPageTransformer { page, position ->
    page.also {
        if (abs(position) >= 1f) {
            it.alpha = 0f
            return@setPageTransformer
        }
        if (position > 0) {
            it.alpha = 1 - position
            val scale = 1f - position / 4f
            it.scaleX = scale
            it.scaleY = scale
            it.translationX = -it.width * position
            it.translationZ = -1f
        } else {
            it.alpha = 1f
            it.scaleX = 1f
            it.scaleY = 1f
            it.translationX = 0f
            it.translationZ = 0f
        }
    }
}
```

## 奥行き＋スライド

奥行きを表現しつつスライドもさせます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/0bbe10b2-97ba-7139-72fc-508c00142a68.gif)

実装は以下になります。左側に抜けていくページは拡大になるので、そのままだとアニメーションが終了するところまで移動させても端っこが画面の中に残ってしまうため、拡大分左側へ移動させています。

```kotlin
binding.viewPager.setPageTransformer { page, position ->
    page.also {
        if (abs(position) >= 1f) {
            it.alpha = 0f
            return@setPageTransformer
        }
        val scale = 1f - position / 4f
        it.scaleX = scale
        it.scaleY = scale
        if (position > 0) {
            it.alpha = 1f - position / 2f
            it.translationX = -1f * it.width * position / 2f
            it.translationZ = -1f
        } else {
            it.alpha = 1f
            it.translationX = it.width * position / 8f
            it.translationZ = 0f
        }
    }
}
```

## 回転

rotaionでページを回転させることもできますね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d7a8712c-814c-3b95-cb1f-f13b5444c093.gif)

実装は以下。スケールが入れ替わるところでtranslationZを変更してレイヤー順を入れ替えています。

```kotlin
binding.viewPager.setPageTransformer { page, position ->
    page.also {
         if (abs(position) >= 1f) {
             it.alpha = 0f
            return@setPageTransformer
         }
         val scale = (1 - abs(position)).coerceAtLeast(MIN_SCALE)
         it.scaleX = scale
         it.scaleY = scale
         it.alpha = 1f
         it.translationX = (1 - scale) * it.width / 2 * if (position > 0) -1 else 1
         it.translationZ = if (scale == MIN_SCALE) -1f else 0f
         it.rotation = -position * 45
    }
}

private const val MIN_SCALE = 0.5f
```

## ページめくり

ページをめくるようなエフェクトですね。実際はページの境界線を表示する必要があるので、プラスアルファの実装が必要です。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/e8e95833-1b6f-e6d9-a3cb-8e6d92d9092c.gif)

実装は以下。translationXでページの移動をしないように見せつつ、clipBoundsで本来のViewの位置で表示をClipすることでページを切り取って表示させています。

```kotlin
binding.viewPager.setPageTransformer { page, position ->
    page.also {
         if (abs(position) >= 1f) {
             it.alpha = 0f
            return@setPageTransformer
         }
         it.alpha = 1f
         it.translationX = -it.width * position
         if (position < 0) {
             it.clipBounds = Rect(it.left, it.top, it.left + it.width, it.bottom)
             it.translationZ = 0f
         } else {
             it.clipBounds = null
             it.translationZ = -1f
         }
    }
}
```

以上です。
カタログと言うにはバリエーション少なかったですね。。