ViewPagerおよびViewPager2ではPageTransformerで比較的シンプルな実装でページ遷移のアニメーションをカスタマイズすることができます。遷移アニメーションとその実装方法を紹介していきます。
PagerTransformerとは
ViewPager.PageTransformerの定義は以下
public interface PageTransformer {
void transformPage(@NonNull View page, float position);
}
ViewPager2.PageTransformerの定義は以下
public interface PageTransformer {
void transformPage(@NonNull View page, float position);
}
完全に一致! まあ意図して同じにしているのでしょうけど
第一引数のpageはそれぞれのページのルートViewが渡され、positionは現在のページを0として1ページ右が1、1ページ左が-1となる値が渡されます。
この位置情報を元にViewに操作を行うことでアニメーションを実装します。
transformPageは現在のページと左右のページに対して呼び出されますが、setOffscreenPageLimitで保持するページ数を変更することで、さらに広い範囲のページも表示させることもできます。
transformPageにて各ページのViewの位置や大きさなどを操作することになりますが、デフォルト動作に対する差分操作を行うことになりますので、例えばスクロールさせずに位置を固定させたい場合は、page.translationX = -it.width * position
の用にデフォルトのスクロールによる移動分を打ち消すような操作を実装します。
positionの小数部が0の状態はアニメーションが終了している状態ですので、この状態では中央以外のページが表示されたり、中央のページの状態が変化の途中になったりしないように実装します。
設定メソッドにはちょっと違いがあります。
public void setPageTransformer(boolean reverseDrawingOrder,
@Nullable PageTransformer transformer)
public void setPageTransformer(@Nullable PageTransformer transformer)
ViewPagerはreverseDrawingOrderを指定します、これは各ページのレイヤー順序を変更できます。
falseではページ番号の大きい方が上に描画されますが、trueをしているすると逆にページ番号の若い方を上に表示されるようになります。
reverseDrawingOrder=false | reverseDrawingOrder=true |
---|---|
ViewPager2ではこのパラメータはなく、falseと同様にページ番号が若い方が上に表示されます。レイヤーの順序を変更したい場合はtranslationZで変更します。現在ではminSdkVersionが21未満であることは珍しいと思うので、ViewPagerもViewPager2もtranslationZでレイヤー順を指定するようにするのが良いかと思います。
実装カタログ
ズームアウト
ViewPagerの説明ページに載っているやつです。ちょっとパラメータの扱いを変更しています。
translationXで隙間を埋める場合 | translationXの補正なし |
---|---|
実装は以下になります。移動に応じてページサイズとアルファを変更しています。
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の説明ページに載っているやつです。同様にちょっとパラメータの扱いを変更しています。
実装は以下になります。左側のページを下に表示するためtranslationZを操作しています。
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
}
}
}
奥行き+スライド
奥行きを表現しつつスライドもさせます。
実装は以下になります。左側に抜けていくページは拡大になるので、そのままだとアニメーションが終了するところまで移動させても端っこが画面の中に残ってしまうため、拡大分左側へ移動させています。
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でページを回転させることもできますね。
実装は以下。スケールが入れ替わるところでtranslationZを変更してレイヤー順を入れ替えています。
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
ページめくり
ページをめくるようなエフェクトですね。実際はページの境界線を表示する必要があるので、プラスアルファの実装が必要です。
実装は以下。translationXでページの移動をしないように見せつつ、clipBoundsで本来のViewの位置で表示をClipすることでページを切り取って表示させています。
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
}
}
}
以上です。
カタログと言うにはバリエーション少なかったですね。。