Android ViewPager 切换动画详解:PageTransformer 从原理到实战

QuibblerQuibbler 2020-01-04 约 21 分钟 222 次阅读

Android ViewPager 切换动画详解:PageTransformer 从原理到实战

ViewPager 默认的页面切换只是"水平平移",朴素到几乎没存在感。想做出"缩放淡化""3D 深度""旋转翻转"这类炫酷效果,靠的就是 PageTransformer——一个只有单个方法的接口,却是 ViewPager 动画的灵魂。本文从 position 参数的数学含义讲起,到两个官方经典实现、再到 ViewPager2 的差异,带你彻底搞懂 ViewPager 切换动画。

1、PageTransformer 是什么

ViewPager.PageTransformer 是一个回调接口,签名极简:在滑动过程中,它会对每个"参与切换的页面"持续调用 transformPage(View page, float position),让你根据 position 给 page 设置旋转、缩放、透明度、位移等变换,从而实时构造动画。本质上,它把"切换效果"完全交给开发者自定义,框架只负责把 position(页面相对位置)喂给你。

public interface PageTransformer {
    /**
     * @param page     被切换的页面 View(当前页、相邻页都会收到回调)
     * @param position 该页面相对正中的位置,随滑动实时变化(见下一节)
     */
    void transformPage(@NonNull View page, float position);
}

它的适用范围很广:既适用于老的 ViewPager(androidx.viewpager.widget),也适用于新的 ViewPager2(androidx.viewpager2.widget),两者用同一套 transformPage 思路,只是设置方法和接口包名略有差异。下面先啃下最关键的 position 参数。

2、核心:position 参数详解

理解 PageTransformer,90% 就是理解 position。它表示"这个 page 相对于屏幕正中"的位置,是一个随手指滑动实时变化的浮点数:

position 取值区间与含义(屏幕正中 = 0):
  position < -1        : 该页在屏幕左侧一屏之外(不可见)
  position = -1        : 该页紧贴屏幕左边界(左侧完全滑入)
  -1 < position < 0   : 该页在左侧、正滑向正中(左页进入)
  position = 0         : 该页正居中、完全显示
  0 < position < 1    : 该页在右侧、正滑离正中(右页离开)
  position = 1         : 该页紧贴屏幕右边界(右侧完全滑出)
  position > 1         : 该页在屏幕右侧一屏之外(不可见)

一个关键认知:transformPage 会被对每个相关页面调用,而且 position 是连续变化的。比如从第 0 页滑到第 1 页时,第 0 页的 position 会从 0 平滑变到 -1(向左离开),第 1 页的 position 会从 1 平滑变到 0(从右侧滑入正中)。你在这个回调里根据 position 计算"缩放比例""旋转角度""透明度"并 setXxx 给 page,连续的回调就拼出了流畅动画。所以写 Transformer 的套路就是:if/else 分段处理 position,每段算出对应变换。

3、setPageTransformer 与绘制顺序

写好 Transformer 后,通过 setPageTransformer 装到 ViewPager 上。注意它有两个参数,第一个 reverseDrawingOrder 控制"是否反转页面绘制顺序":

// ViewPager(androidx.viewpager.widget)
viewPager.setPageTransformer(true, new ZoomOutPageTransformer());
//                       ^^^^
// reverseDrawingOrder = true:反转绘制顺序,保证"即将进入的页面"画在最上层,
//   否则缩放/位移后的页面可能被相邻页遮挡,看起来怪异。
// 多数自定义 Transformer 都传 true。

// 取消自定义动画,恢复默认平移:
viewPager.setPageTransformer(true, null);

一个常见坑:Transformer 里如果只做"缩放/透明"不涉及 z-order,传 true 或 false 差别不大;但一旦做了"位移"让页面重叠,就必须用 true 反转绘制顺序,否则进入页会被压在底下。

4、经典效果一:ZoomOutPageTransformer(缩放淡化)

这是官方示例里最经典的一个,效果是"页面滑出时逐渐缩小并变透明",离正中越远越小越淡。它的实现就是按 position 分段:屏幕外 alpha=0;正中位置不缩放;中间按距离算缩放与透明度。

public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.85f;  // 最小缩放
    private static final float MIN_ALPHA = 0.5f;   // 最小透明度

    @Override
    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        int pageHeight = view.getHeight();

        if (position < -1) {                 // 屏幕左侧外:完全隐藏
            view.setAlpha(0f);
        } else if (position <= 1) {           // [-1,1]:可见区间
            // 离正中越远,缩得越小(不低于 MIN_SCALE)
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth  * (1 - scaleFactor) / 2;
            if (position < 0) {
                view.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                view.setTranslationX(-horzMargin + vertMargin / 2);
            }
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);
            // 透明度:缩到最小时降到 MIN_ALPHA
            view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE)
                    / (1 - MIN_SCALE) * (1 - MIN_ALPHA));
        } else {                              // 屏幕右侧外:完全隐藏
            view.setAlpha(0f);
        }
    }
}

理解要点:核心是 scaleFactor = max(MIN_SCALE, 1 - |position|)——用"距离正中的远近"直接换算成"缩放比例";再用缩放差算出留白边距(horzMargin/vertMargin),让缩小的页面居中显示而非贴边。这个"距离换变换"的思路,是所有缩放类 Transformer 的通用模板。

5、经典效果二:DepthPageTransformer(3D 深度)

另一个官方示例,效果是"当前页从右滑入、停留在正中;右侧页在它身后缩小淡化",有明显的 3D 纵深感。它对左页(position < 0)保持原样、对右页(0~1)做淡出+缩放+左移,分段更分明:

public class DepthPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    @Override
    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        if (position < -1) {                 // 左侧外:隐藏
            view.setAlpha(0f);
        } else if (position <= 0) {          // [-1,0]:当前/左页,原样显示
            view.setAlpha(1f);
            view.setTranslationX(0f);
            view.setScaleX(1f);
            view.setScaleY(1f);
        } else if (position <= 1) {          // (0,1]:右页,淡化+左移+缩小
            view.setAlpha(1 - position);
            view.setTranslationX(pageWidth * -position);
            float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);
        } else {                              // 右侧外:隐藏
            view.setAlpha(0f);
        }
    }
}

对比 ZoomOut:ZoomOut 是"两边都缩",Depth 是"只缩右侧(身后页)",所以 Depth 的正中页始终"顶在最前"、身后页渐渐退后,纵深感更强。把第 4、5 节两个实现轮流换着用,就能直观感受 Transformer 的威力。

6、更多效果:旋转、3D 翻转、卡片堆叠

掌握了"按 position 分段算变换"的套路,各种效果都好写。最简洁的是旋转类——一行 setRotationY 就有强烈的 3D 翻转感:

// 绕 Y 轴翻转:position=0 时正面朝上,向两侧逐渐翻转 90 度
public void transformPage(View page, float position) {
    // 限制只在 [-1,1] 内处理,避免屏幕外浪费
    if (position < -1 || position > 1) {
        page.setAlpha(0f);
        return;
    }
    page.setCameraDistance(page.getWidth() * 20); // 拉远相机,避免翻转失真
    page.setRotationY(position * -90);            // ±90 度翻转
    page.setAlpha(1 - Math.abs(position));
}

// 卡片堆叠:当前页滑出时向下/向右"塌"到一堆里
// 思路:page.setTranslationY(position * pageHeight * 0.3f) + 缩小 + 半透明

小技巧:做 3D 旋转时记得 setCameraDistance 把"相机"拉远,否则近大远小的透视会让翻转中的页面严重变形。把 rotation/translation/scale/alpha 组合起来,就能演变出翻转、堆叠、抽屉、画廊等几十种效果——本质都是在 transformPage 里按 position 算几个变换。

7、ViewPager2 的差异与用法

新项目多用 ViewPager2(基于 RecyclerView,更现代)。它的 PageTransformer 思路完全一致,但接口更简洁——setPageTransformer 只剩一个参数(绘制顺序由框架自动处理,不用手动 reverse),并且官方提供了 MarginPageTransformer 用于"页面间距",还支持纵向滑动:

// ViewPager2(androidx.viewpager2.widget)
// 自定义动画:复用第 4/5 节的 Transformer(接口名变成 ViewPager2.PageTransformer)
viewPager2.setPageTransformer(new ZoomOutTransformer());

// 页面间距:相邻页面之间留 40px 缝隙(官方内置,无需自己写)
viewPager2.setPageTransformer(new MarginPageTransformer(40));

// 纵向滑动(上下翻页),配合 Transformer 能做"垂直卡片"效果
viewPager2.setOrientation(ViewPager2.ORIENTATION_VERTICAL);

// 注意:ViewPager2.PageTransformer 与 ViewPager.PageTransformer 接口结构相同,
// 都是 transformPage(View page, float position),旧 Transformer 改个 import/接口名即可迁移。

迁移要点:旧 Transformer 的逻辑(按 position 分段算变换)可以原样复用,只需把实现的接口从 `androidx.viewpager.widget.ViewPager.PageTransformer` 换成 `androidx.viewpager2.widget.ViewPager2.PageTransformer`,再去掉 reverseDrawingOrder 这个参数即可。

8、实战与最佳实践

       - 选效果:引导页/相册用 ZoomOut(轻柔),主 Tab 切换用 Depth 或不加 Transformer(稳重),卡片展示用旋转/堆叠(炫酷)

       - 性能:Transformer 在滑动期间高频回调,内部只做"几个 setXxx",避免创建对象、避免复杂计算;3D 旋转依赖硬件加速,确保页面未关闭硬件层

       - 边界:务必处理 position < -1 与 > 1(屏幕外),及时 setAlpha(0) 隐藏,否则可能渲染不可见页面、浪费性能

       - 新项目优先 ViewPager2 + FragmentStateAdapter,Transformer 可平滑迁移

       - 调参套路:先把官方 ZoomOut/Depth 拷进去跑通,再调 MIN_SCALE/MIN_ALPHA 两个常量找到顺眼的"缩放感"和"淡出感"

9、总结

ViewPager 的切换动画,本质就是把"页面相对位置"换算成"页面变换"。PageTransformer 只有一个 transformPage 方法,难点全在理解 position——0 是正中、-1/1 是左右屏外、中间连续变化。在这个回调里按 position 分段算缩放/旋转/位移/透明度,就拼出了 ZoomOut、Depth、翻转、堆叠等所有效果。

关键要点:

       - PageTransformer.transformPage(page, position):position 0=正中,±1=屏外,连续变化

       - 套路:if/else 分段处理 position,每段 setScaleX/setRotationY/setTranslationX/setAlpha

       - ZoomOut:两边都缩;Depth:只缩身后页、纵深感强;setPageTransformer 传 true 反转绘制

       - ViewPager2:setPageTransformer 单参数、官方 MarginPageTransformer、支持纵向;旧 Transformer 可平滑迁移

       - 性能:高频回调内别创建对象、处理好屏外边界(setAlpha(0))

对于做引导页、Banner、卡片浏览这类需要"动起来"的 Android 界面而言,PageTransformer 是性价比极高的工具——几十行代码就能把朴素的平移切换变成有质感的高级动画。把 position 的数学含义吃透、再备好 ZoomOut 与 Depth 两个"基准实现",绝大多数切换效果你都能信手拈来。下次产品要"切换酷一点",别再满世界找轮子,自己写个 Transformer 往往更快、更可控。

学习资料:

        Android开发之ViewPager切换动画

        viewpager过渡动画——PagerTransformer实现

        https://www.jianshu.com/p/1c23d5ec3bd8

相关推荐

精选
ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager
Android

ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager

ViewPager1、ViewPagerandroidx.viewpager.widget.ViewPager,Android中使用非常广泛的控件,可以说是APP必备:首次打开引导页、页面Banner广告等。常用方法:setAdapter() 设置适配器setOffscreenPageLimit() 设置缓存的页面个数,默认是 1setCurrentItem() 跳转到特定的页面setOnPage

1.1k
获取Android内置WebView内核版本
Android

获取Android内置WebView内核版本

获取Android内置WebView内核版本竟然能遇到这样奇葩的事情,网页用的技术过于新颖,以至于只支持高版本Chromium内核的,低版本安卓系统中内置的内核版本较低,无法加载前端页面。1、设置查看在系统设置里 > 应用 > 应用管理 > 显示系统应用,查看WebView组件:2、页面查看通过WebView发起的网络请求,都会带上浏览器的UA,通常页面都可以通过UA判断浏览器的内核版本。这里有两

1.1w
Android 14适配总结
Android

Android 14适配总结

Android 14适配总结毕业工作至今已经适配了三个Android大版本,从Android 11到Android 12、再到Android 13。2023年,Google即将推出的Android 14,上半年已经开始第一批适配。现在,第四个Android版本已经适配完,总结记录一下。1、Android 14计划Google一般会在2月份对外发布预告,同时放出开发者预览版。“拉通”各大平台、厂商以

1.1w