【动画图解】这个值取对了,ViewPager2才能纵享丝滑

前言

在前两篇文章中,我们通过一张张清晰明了的「示意图」,详细地复盘了RecyclerView「缓存复用机制」与「预拉取机制」的工作流程,这种「图解」创作形式也得到了来自不同平台读者们的一致认可。

而从本文开始,我们将正式进入ViewPager2的篇章,并将辅以更加生动易懂的「动态示意图」来进行讲解。

ViewPager2可讲的内容有很多,今天我们主要介绍是ViewPager2的「离屏加载机制」,你可能是第一次听说这个术语,但在实际开发中,你肯定使用过它,因为它对应的配置入口,就是ViewPager2的OffscreenPageLimit属性。

OffscreenPageLimit是什么?

OffscreenPageLimit,直译过来是离屏页面限制值的意思,该值代表的是在滑动视图中应保留在当前可见页面之外的任一方向上的页面数

比如,当我们采用水平分页时,该值代表的便是在左右两侧应保留的页面数。

而当我们采用垂直分页时,该值代表的则是在上下两侧应保留的页面数。

保留页面的方式是通过扩展额外的布局空间实现的,以LinearLayoutManager为例,其最关键的步骤在于对calculateExtraLayoutSpace方法的重写:

    /**
    * 计算额外的布局空间
    */
    @Override
    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
            @NonNull int[] extraLayoutSpace) {
        int pageLimit = getOffscreenPageLimit();
        if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            // 仅在需要时才对屏幕外页面进行自定义预取
            super.calculateExtraLayoutSpace(state, extraLayoutSpace);
            return;
        }
        // 计算多pageLimit*2个页面大小的空间
        final int offscreenSpace = getPageSize() * pageLimit;
        extraLayoutSpace[0] = offscreenSpace;
        extraLayoutSpace[1] = offscreenSpace;
    }
    
    /**
    * 获取单个页面大小
    */
    int getPageSize() {
        final RecyclerView rv = mRecyclerView;
        // 水平分页时,取去除了左右内边距后的RecyclerView宽度
        // 垂直分页时,取去除了上下内边距后的RecyclerView高度
        return getOrientation() == ORIENTATION_HORIZONTAL
                ? rv.getWidth() - rv.getPaddingLeft() - rv.getPaddingRight()
                : rv.getHeight() - rv.getPaddingTop() - rv.getPaddingBottom();
    }

该方法会计算LinearLayoutManager应布置的额外空间量(以像素为单位)。已知默认布置的空间量为单个页面大小,则额外布置的空间量应为OffscreenPageLimit*2个单页面大小,计算出来的结果会存储在int数组类型的extraLayoutSpace结构中,其中:

  • extraLayoutSpace[0]应用于顶部或左侧的额外空间;
  • extraLayoutSpace[1]应用于底部或右侧的额外空间。

虽然这部分额外创建的页面在当前屏幕上并不可见,但实际已经被添加至我们的视图层次结构中了。这么做可以减少切换分页时花费在视图创建与布局上的时间,从而提升ViewPager2滑动时的整体流畅度

结合前面两篇文章我们可以看到,从缓存复用机制到预拉取机制再到现在的离屏加载机制,RecyclerView与ViewPager2在提升滑动流畅度方面真的是做了非常多的努力。

区别在于:

  • 缓存复用机制是通过缓存已创建的页面,以提供给新进入屏幕的页面重用来实现的。
  • 预拉取机制是通过利用UI线程空闲的时机,提前创建并缓存下一个待进入屏幕的页面来实现的。
  • 离屏加载机制则是通过扩展额外的布局空间,以提前创建并保留屏幕两侧的页面来实现的。

从调用方法流程上讲,离屏加载机制除了常规的onCreateViewHolder、onBindViewHolder方法之外,还会执行一个多onViewAttachedToWindow方法,以将页面提前添加至我们的视图层次结构中。

虽然我们一直强调的是“ViewPager2的离屏加载机制”,但其实,离屏加载机制并不是ViewPager2才引入的新特性,作为ViewPager的改进版本,ViewPager2也只是把早已存在于ViewPager中的这个特性照搬过来而已,二者的主要区别有以下几点:

  • 对于OffscreenPageLimit默认值的设置
  • 对于OffscreenPageLimit赋值条件的限制

OffscreenPageLimit的默认值设置与赋值条件限制

ViewPager一直为人所诟病的一个点就是,其设置的OffscreenPageLimit默认值为1,且不允许外部传入低于1的修改值,即会强制开启离屏加载机制

    // 默认的离屏加载限制值为1
    private static final int DEFAULT_OFFSCREEN_PAGES = 1;

    public void setOffscreenPageLimit(int limit) {
        // 小于默认值的数会被强制设为默认值
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                    + DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES;
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }

// 谁赞成,谁反对

这也就意味着,在使用ViewPager构建的滑动视图中,不管开发者需不需要,都至少会有1~2个页面会被离屏加载,而这会导致一系列依赖于Fragment生命周期的逻辑被异常执行,进而产生非预期的结果,需要开发者手动实现延迟加载机制。

相比较之下,ViewPager2设置的OffscreenPageLimit默认值则为-1,也即默认不开启离屏加载机制,且对于外部传入的修改值也只要求必须是大于0的正数或默认值。

     // 默认的离屏加载限制值为-1
    public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;

    public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
        // 低于1且非默认值的传参会报异常
        if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            throw new IllegalArgumentException(
                    "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
        }
        mOffscreenPageLimit = limit;
        // 触发重新布局操作,以便通过getExtraLayoutSize()方法进行离屏加载
        mRecyclerView.requestLayout();
    }

另外,我们在本系列的第一篇就讲了,ViewPager2是在RecyclerView的基础上构建而成的。因此,即使是默认不开启离屏加载机制,预拉取机制也会正常工作。

而我们前面又讲了,预拉取机制会提前创建并缓存下一个待进入屏幕的页面,但不会添加至我们的视图层次结构中,因此不会像ViewPager一样,导致一系列依赖于Fragment生命周期的逻辑被异常执行,相当于自动帮我们实现延迟加载机制了。

从以上2个默认数值我们可以看到,无论是ViewPager还是ViewPager2,其对于OffscreenPageLimit默认值的设置都是比较克制的。实际上,在setOffscreenPageLimit方法的注释中,Android也是建议我们将此限制值保持在较低水平,尤其是当我们的页面具有复杂的布局时。

但实际情况是,大部分的开发者为图方便,往往会将此值设为页面总数-1,也即默认会离屏加载所有的页面

这种做法无疑是很不规范的,为什么说不规范呢?这就引申出我们下一个问题了,即OffscreenPageLimit的不同赋值,会对ViewPager2产生什么样的影响呢?

不同的OffscreenPageLimit值产生的影响

行为表现
OffscreenPageLimit值为-1

当OffscreenPageLimit值为-1时,也即保持默认不开启离屏加载机制,这种情况下只有RecyclerView的缓存复用机制和预拉取机制会工作。

  1. 当滑动视图初始化完成时,只有position=0的页面项会被添加至当前视图层次结构中。
  2. 随着我们往左滑动屏幕,预拉取机制会开始工作,提前创建position=2的页面项并放入mCachedView中。
  3. 同时,position=0的页面项也将随着向左滑动的手势被移出屏幕,并放入mCachedView中。

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=2的页面项进行使用,同时开启对position=3的页面项的预拉取。
  2. 此时,由于还未超过mCachedView大小的限制,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

  1. 第三次向左滑动屏幕,同样,会取出预拉取的position=3的页面项进行使用,同时开启对position=4的页面项的预拉取。
  2. 但是,由于超过了mCachedView大小的限制,在下一个被移出屏幕的position=2的页面项尝试进入时,会先按照先进先出的顺序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中,然后position=2的页面项才顺利进入mCachedView。
  3. 之后的滑动同样遵循这个规律,不再赘述。
OffscreenPageLimit值为1

当OffscreenPageLimit值为1时,也即会在左右两侧各离屏加载1个页面。

  1. 当滑动视图初始化完成时,由于左侧无更多的页面项,因此只有position=0及position=1的页面项会被添加至当前视图层次结构中。
  2. 随着我们往左滑动屏幕,position=2的页面项会被添加至当前视图层次结构中,而position=0的页面项会继续保留在当前视图层次结构中,同时预拉取机制会开始工作,提前创建position=3的页面项并放入mCachedView中。

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=3的页面项添加至当前视图层次结构中,而position=1的页面项会继续保留在当前视图层次结构中,并开启对position=4的页面项的预拉取。
  2. 同时,position=0的页面项也将随着向左滑动的手势被移出屏幕,并放入mCachedView中。

  1. 第三次向左滑动屏幕,同样,会取出预拉取的position=4的页面项添加至当前视图层次结构中,并保留position=2的页面项在当前视图层次结构中,同时开启对position=5的页面项的预拉取。
  2. 此时,由于还未超过mCachedView大小的限制,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

  1. 第四次向左滑动屏幕,同样,会取出预拉取的position=5的页面项添加至当前视图层次结构中,并保留position=3的页面项在当前视图层次结构中,同时开启对position=6的页面项的预拉取。
  2. 但是,由于超过了mCachedView大小的限制,在下一个被移出屏幕的position=2的页面项尝试进入时,会先按照先进先出的顺序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中。
OffscreenPageLimit值为3

当OffscreenPageLimit值为3时,也即会在左右两侧各离屏加载3个页面。

  1. 当滑动视图初始化完成时,由于左侧无更多的页面项,因此只有position=0至position=3的页面项会被添加至当前视图层次结构中。
  2. 随着我们往左滑动屏幕,position=4的页面项会被添加至当前视图层次结构中,而position=0的页面项会继续保留在当前视图层次结构中,同时预拉取机制会开始工作,提前创建position=5的页面项并放入mCachedView中。

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=5的页面项添加至当前视图层次结构中,而position=1的页面项会继续保留在当前视图层次结构中,并开启对position=6的页面项的预拉取。

  1. 第三次向左滑动屏幕,滑动视图会取出预拉取的position=6的页面项添加至当前视图层次结构中,而position=2的页面项会继续保留在当前视图层次结构中。也即这个时候,所有的页面项已经都被添加至当前视图层次结构中了。

  1. 第四次向左滑动屏幕,由于超出了OffscreenPageLimit值,position=0的页面项将随着向左滑动的手势被移出屏幕,并放入mCachedView中。

  1. 第五次向左滑动屏幕,此时,由于还未超过mCachedView大小的限制,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

  2. 第六次向左滑动屏幕,由于开启了预拉取机制,mCachedView大小的限制由默认的2项再加上预拉取的1项,变为3项,因此仍未超过mCachedView大小的限制,下一个被移出屏幕的position=2的页面项仍将放入mCachedView中,此处不再进行动画展示。

OffscreenPageLimit值为页面总数-1

当OffscreenPageLimit值为页面总数-1时,也即在滑动视图初始化完成时就已经离屏加载所有的页面了,这种情况下RecyclerView的缓存复用机制和预拉取机制完全没有工作的机会。

虽然设置更高的OffscreenPageLimit值,可以更好地提升ViewPager2滑动时的流畅度,但由于需要在初始化阶段同时创建多个页面项,意味着将花费更久的创建时间,页面项内容也将更慢显示,同时,由于两侧有更多的页面项被保留而不走缓存复用流程,意味着应用会占用更多的内存,且这些问题将随着页面复杂度提升更加突出。

为了更直观地展示不同的OffscreenPageLimit值对应用的性能影响,我们将从白屏时间、流畅度、占用内存三个维度来进行横向对比:

性能影响
白屏时间

可以看到,随着OffscreenPageLimit值的增加,在滑动视图的初始化阶段,会有更多的页面项需要被创建并被添加至当前的视图层次结构中,白屏时间也随之延长。

流畅度

参考上一篇的做法,我们同样在FragmentStateAdapter中对Fragment的视图准备工作做了延迟,以在GPU渲染模式中展示更加清晰的柱状图:

OffscreenPageLimit值为1时,虽然可以离屏加载下一个页面,但由于每次滑动还要执行预拉取的工作,因此对于流畅度的提升不是很明显。

OffscreenPageLimit值为3时,即每次都会保留当前屏幕两侧的各3个页面项,在滑动到中间位置时,对于流畅度的提升是最大的,此时无论是往前滑还是往后滑,都无需再执行页面项的创建工作,即使滑到边界也可以利用缓存复用机制来重用视图。

OffscreenPageLimit值为6时,也即在滑动视图初始化完成时就已经离屏加载所有的页面了,每次的滑动就相当于只是在当前的视图层次结构中进行位移,因此全程的流畅度都有极大的提升。

内存占用

可以看到,随着OffscreenPageLimit值的增加,在滑动视图的初始化阶段,会有更多的Fragment对象驻留在内存中。

同时,由于OffscreenPageLimit值会保留当前屏幕两侧的页面项,因此,滑动到中间位置时,OffscreenPageLimit值为1的情况最多会保留3个Fragment对象,而OffscreenPageLimit值为3的情况最多会保留7个Fragment对象。

但在其他位置时,它会将超出OffscreenPageLimit值限制的页面将从视图层次结构中移除,并交由RecyclerView的缓存复用机制处理,同往常一样回收ViewHolders对象以供重用。

OffscreenPageLimit值取多大比较合适?

现在我们知道了,当OffscreenPageLimit值设得过大,比如页面总数-1时,会给应用带来比较大的内存压力,特别是在部分低端机型上。

而OffscreenPageLimit值设得过小,比如1时,又无法发挥出离屏加载机制提高页面滑动流畅度的优势。

一般来讲,同时保持3-4个页面项处于活动状态是一个比较合适的值,一方面,可以提高用户来回翻页时的流畅度,另一方面又不会给应用带来太大的内存压力。当然,还需要我们自己维护好Fragment重建以及视图回收/复用时的处理逻辑。

最好的情况下,还是希望能够根据应用当前的内存使用情况,对该值进行动态调整,在行为表现与性能影响上取一个平衡点。

但如果多个页面项之间存在互斥关系,同时处于活动状态可能影响业务的判断时,保持OffscreenPageLimit为默认值,也即默认关闭离屏加载机制,只让预拉取机制与缓存复用机制工作,也许是个更好的选择。

后记

讲到这里,相信你对ViewPager2的离屏加载机制已经有了一定的认识,但不知道你发现没有,我们全文讲的都是ViewPager2顺序依次翻页的情况,但在实际运用中,我们常常会搭配TabLayout,提供点击标签页跳转到指定页面项的功能。

而当增加了这一种新的交互方式后,问题的维度再一次上升了,我们会发现离屏加载机制的行为逻辑又有所不同了,而这,就是下一篇的内容了。

少侠,请留步!若本文对你有所帮助或启发,还请:

  1. 点赞👍🏻,让更多的人能看到!
  2. 收藏⭐️,好文值得反复品味!
  3. 关注➕,不错过每一次更文!

===> 技术号:「星际码仔」💪

你的支持是我继续创作的动力,感谢!🙏

<think>我们使用TabLayout和ViewPager2组合时,ViewPager2内部默认使用FragmentStateAdapter来管理Fragment。ViewPager2本身没有直接提供设置Fragment切换动画时长的方法,因为动画时长是由其内部的RecyclerView的ItemAnimator控制的。但是我们可以通过自定义页面切换动画或者调整RecyclerView的ItemAnimator来影响动画时长。然而,需要注意的是,ViewPager2的页面切换动画实际上是由两个部分组成:1.页面滑动动画(由ViewPager2的PageTransformer控制,用于页面之间的滑动切换)2. Fragment的入场和退场动画(由Fragment的动画资源控制,但ViewPager2默认使用FragmentStateAdapter,它不会为Fragment添加标准的FragmentTransaction动画)实际上,ViewPager2在切换Fragment时并不会使用FragmentTransaction的标准动画(即setCustomAnimations),而是通过RecyclerView的ItemAnimator来管理Fragment的添加和移除动画。因此,要改变动画时长,我们需要自定义RecyclerView的ItemAnimator。具体步骤:1.获ViewPager2内部的RecyclerView。2.获RecyclerView的ItemAnimator,并设置其动画时长。但是,ViewPager2并没有直接暴露RecyclerView,我们可以通过反射或者通过ViewPager2的子View来获RecyclerView。然后,我们可以修改ItemAnimator的动画时长。另外,如果我们想要自定义页面切换的滑动动画,可以使用ViewPager2的setPageTransformer方法,但这里主要关注Fragment的添加和移除动画(即ItemAnimator控制的动画)。示例代码:首先,在布局文件中添加TabLayout和ViewPager2。然后,在Activity或Fragment中:步骤1:设置ViewPager2的Adapter(使用FragmentStateAdapter)。步骤2:将TabLayout和ViewPager2连接起来。步骤3:获ViewPager2内部的RecyclerView,并修改其ItemAnimator。注意:修改ItemAnimator的动画时长会影响所有Item的动画,包括添加、移除、移动和改变动画。由于ViewPager2内部使用RecyclerView,我们可以这样获:```kotlin//获ViewPager2的第一个子View,即RecyclerViewvalrecyclerView= viewPager2.getChildAt(0) as? RecyclerViewrecyclerView?.itemAnimator?.let{//修改动画时长it.addDuration=500 //添加动画时长(毫秒)it.removeDuration=500 //移除动画时长(毫秒)it.moveDuration=500 //移动动画时长(毫秒)it.changeDuration=500 //改变动画时长(毫秒)} ```但是,ViewPager2在切换页面时,Fragment的添加和移除动画就是由RecyclerView的添加和移除动画控制的。因此,修改addDuration和removeDuration就可以改变Fragment的动画时长。另外,如果我们想要更精细的控制,比如只改变页面切换时Fragment的动画,我们可以自定义ItemAnimator,但这样相对复杂。另一种思路:使用FragmentStateAdapter的setFragmentMaxLifecycle来控制Fragment的生命周期,但是这样并不能直接改变动画时长。因此,修改RecyclerView的ItemAnimator的动画时长是最直接的方法。完整示例代码:```kotlinclassMainActivity: AppCompatActivity(){override funonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val tabLayout =findViewById<TabLayout>(R.id.tab_layout)val viewPager2 =findViewById<ViewPager2>(R.id.view_pager)//设置AdapterviewPager2.adapter= MyPagerAdapter(this)//连接TabLayout和ViewPager2TabLayoutMediator(tabLayout,viewPager2) {tab,position ->tab.text ="Tab${position +1}"}.attach()//修改ViewPager2内部RecyclerView的ItemAnimator动画时长viewPager2.post{val recyclerView =viewPager2.getChildAt(0)as?RecyclerViewrecyclerView?.itemAnimator?.let{//设置所有动画时长it.addDuration =500it.removeDuration =500it.moveDuration =500it.changeDuration =500}}} }class MyPagerAdapter(fragmentActivity: FragmentActivity): FragmentStateAdapter(fragmentActivity){override fungetItemCount():Int =3override funcreateFragment(position:Int):Fragment {returnwhen (position){0 ->FirstFragment()1-> SecondFragment()else-> ThirdFragment()}}} ```注意:由于ViewPager2在设置Adapter之后才会创建RecyclerView,所以我们使用post方法确保在ViewPager2布局完成后再获RecyclerView。另外,我们也可以自定义一个ItemAnimator并设置给RecyclerView,例如:```kotlinrecyclerView?.itemAnimator= DefaultItemAnimator().apply {addDuration =500removeDuration=500moveDuration =500changeDuration=500} ```这样我们就可以控制动画时长了。但是,这种方法只能改变Fragment的添加和移除动画(即新页面进入和旧页面离开的动画),而页面之间的滑动动画是由ViewPager2的PageTransformer控制的,并且滑动动画的时长由滑动手势的速度ViewPager2滑动动画器决定。如果我们想改变滑动动画的时长,可以通过自定义PageTransformer来实现,但通常滑动动画的时长是由用户滑动手势的速度决定的,程序难以固定时长。总结:1.对于Fragment的添加和移除动画(即页面切换时Fragment的淡入淡出效果),通过修改RecyclerView的ItemAnimator的addDuration和removeDuration可以改变动画时长。2.对于页面之间的滑动动画ViewPager2默认使用线性滑动,其时长由滑动手势的速度决定。如果我们希望固定滑动动画时长,可以通过代码模拟滑动(例如使用ViewPager2的setCurrentItem方法并配合一个固定时长的动画),但这通常不是用户手势触发的。如果问题中的“动画时长”指的是页面滑动动画的时长,那么我们需要使用另一种方式:使用ObjectAnimator来模拟滑动,并固定时长。但是,这会影响用户手势滑动的体验,因此通常不推荐。因此,本解决方案主要针对Fragment的添加和移除动画(由ItemAnimator控制)的时长调整。 §§相关问题§§:1.如何自定义ViewPager2的页面滑动动画2.在ViewPager2中如何实现Fragment的切换动画(如淡入淡出)?3.如何禁用ViewPager2滑动动画?4.如何为ViewPager2的每个Fragment设置不同的进入和退出动画?</think>### 设置 TabLayout + ViewPager2 的 Fragment 动画时长 在 Android 中,TabLayout 和 ViewPager2 组合使用时,**默认情况下无法直接设置 Fragment 切换的动画时长**。ViewPager2 内部使用 RecyclerView 实现,其页面切换动画由 RecyclerView 的 ItemAnimator 控制,而不是标准的 Fragment 动画。 不过,可以通过以下两种方法实现自定义动画时长: #### 方法 1:自定义 Fragment 动画(推荐) 这是最灵活的方法,通过重写 FragmentStateAdapter 的 `onCreateFragment()` 方法自定义动画: ```kotlin class CustomFragmentStateAdapter( fragmentActivity: FragmentActivity, private val enterAnim: Int, private val exitAnim: Int ) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int = 3 // 根据实际标签数量修改 override fun createFragment(position: Int): Fragment { return when (position) { 0 -> FirstFragment().apply { // 设置自定义动画 enterTransition = MaterialFadeThrough().apply { duration = 366 // 设置进入动画时长 } exitTransition = MaterialFadeThrough().apply { duration = 366 // 设置退出动画时长 } } 1 -> SecondFragment().apply { enterTransition = Slide(Gravity.END).apply { duration = 366 } exitTransition = Slide(Gravity.START).apply { duration = 366 } } else -> ThirdFragment().apply { enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true).apply { duration = 366 } exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false).apply { duration = 366 } } } } } ``` **使用方式:** ```kotlin val viewPager2 = findViewById<ViewPager2>(R.id.view_pager) val tabLayout = findViewById<TabLayout>(R.id.tab_layout) // 创建自定义适配器 val adapter = CustomFragmentStateAdapter( this, enterAnim = android.R.anim.fade_in, exitAnim = android.R.anim.fade_out ) viewPager2.adapter = adapter // 连接TabLayout和ViewPager2 TabLayoutMediator(tabLayout, viewPager2) { tab, position -> tab.text = "Tab ${position + 1}" }.attach() ``` #### 方法 2:自定义 ViewPager2 的页面切换动画 通过自定义 PageTransformer 控制页面切换动画的时长: ```kotlin class CustomPageTransformer(private val duration: Long) : ViewPager2.PageTransformer { override fun transformPage(page: View, position: Float) { // 自定义转换逻辑(可选) } // 通过反射修改内部动画时长 fun setAnimationDuration(viewPager: ViewPager2) { try { val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView") recyclerViewField.isAccessible = true val recyclerView = recyclerViewField.get(viewPager) as RecyclerView val layoutManagerField = ViewPager2::class.java.getDeclaredField("mLayoutManager") layoutManagerField.isAccessible = true val layoutManager = layoutManagerField.get(viewPager) as LinearLayoutManager val scrollerField = layoutManager::class.java.getDeclaredField("mSmoothScroller") scrollerField.isAccessible = true val scroller = scrollerField.get(layoutManager) as PagerSnapHelper val animatorField = scroller::class.java.getDeclaredField("mAnimator") animatorField.isAccessible = true val animator = animatorField.get(scroller) as ValueAnimator animator.duration = duration } catch (e: Exception) { e.printStackTrace() } } } ``` **使用方式:** ```kotlin val viewPager2 = findViewById<ViewPager2>(R.id.view_pager) val transformer = CustomPageTransformer(366) // 设置动画时长 viewPager2.setPageTransformer(transformer) transformer.setAnimationDuration(viewPager2) // 应用自定义时长 ``` ### 动画效果说明 1. **Fragment 过渡动画类型**: - `MaterialFadeThrough`:淡入淡出效果 - `Slide`:滑动效果 - `MaterialSharedAxis`:3D 轴旋转效果 - `Fade`:标准淡入淡出 2. **动画时长控制**: ```kotlin enterTransition.duration = 366 // 366毫秒 exitTransition.duration = 366 ``` 3. **支持的方向**: ```kotlin Slide(Gravity.START) // 从左进入 Slide(Gravity.END) // 从右进入 Slide(Gravity.TOP) // 从上进入 Slide(Gravity.BOTTOM)// 从下进入 ``` ### 注意事项 1. **兼容性**: - 方法1 (Fragment 过渡动画) 需要 Android 5.0+ (API 21+) - 方法2 (反射修改) 可能在不同 Android 版本上失效 2. **性能优化**: ```kotlin // 在Fragment中启用过渡动画优化 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) postponeEnterTransition() // 延迟动画开始 } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 内容加载完成后开始动画 (view.parent as? ViewGroup)?.doOnPreDraw { startPostponedEnterTransition() } } ``` 3. **自定义动画资源**: 可以创建自定义 XML 动画资源: ```xml <!-- res/anim/slide_in_right.xml --> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:duration="366" android:fromXDelta="100%" android:toXDelta="0%"/> </set> ``` 然后在代码中使用: ```kotlin enterTransition = AnimationUtils.loadAnimation(context, R.anim.slide_in_right) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值