美文网首页
ViewPager2修改翻页动画时间

ViewPager2修改翻页动画时间

作者: FENGAO | 来源:发表于2021-09-23 21:04 被阅读0次

    ViewPager2是google推出的替代viewpager的库。功能相比viewpager强大了不少。但是有个比较难受的点就是当使用setCurrentItem翻页时,viewpager2是不支持设置翻页动画时长的,并且动画时长非常快,这就导致了部分场景下快速翻页的效果不是特别合适。viewpager可以通过反射替换viewpager的”mScroller“字段完成动画时长的设置。

     try {
       val field = ViewPager::class.java.getDeclaredField("mScroller");
       field.isAccessible = true
       val scroller = MyScroller(context) 
       field.set(viewpager, scroller);
       scroller.setmDuration(800);
     } catch (e: Exception) {
       e.printStackTrace()
     }
    

    但很遗憾的是viewpager2不支持这样去做,并且通过查阅资料viewpager2的作者明确表示没有这个功能的开发计划。 https://issuetracker.google.com/issues/122656759

    jg...@google.comjg...@google.com #4Aug 7, 2019 07:07PM

    Status: Won't Fix (Infeasible)

    Unlikely to address due to bandwidth constraints - icebox for now.

    去网上搜了一下解决方案,大部分都是使用FakeDrag 系列的api完成的这个功能,但是看了下网上的代码只是特别简单的实现了一个翻页动画,其中关于连续翻页或者翻页动画未完成时输入反方向动画事件,以及连续输入多对相反方向的操作事件的情况都没有处理。说白了根本不能用。

    这个时候我就面临了两个选择:改回viewpager或者想办法解决这个问题。对比了一下这两个选项的工作量,感觉改回viewpager工作量是可见的但也是比较多的,解决这个问题呢,可能路子比较难走,但是假如走通了改动量应该是比较小的。所以觉得先尝试着解决一下这个问题。

    根据viewpager上处理动画时间的经验,感觉viewpager2也有一个类似的对象来控制滚动时长,找到这个对象然后反射替换应该就行了。但是通过查看viewpager2源码其实可以看到viewpager2其实最终是调用了RecycleView的smoothScrollToPosition进行的滚动。

        public void setCurrentItem(int item, boolean smoothScroll) {
            if (isFakeDragging()) {
                throw new IllegalStateException("Cannot change current item when ViewPager2 is fake "
                        + "dragging");
            }
            setCurrentItemInternal(item, smoothScroll);
        }
    
    void setCurrentItemInternal(int item, boolean smoothScroll) {
    
            ...
            // For smooth scroll, pre-jump to nearby item for long jumps.
            if (Math.abs(item - previousItem) > 3) {
                mRecyclerView.scrollToPosition(item > previousItem ? item - 3 : item + 3);
                // TODO(b/114361680): call smoothScrollToPosition synchronously (blocked by b/114019007)
                mRecyclerView.post(new SmoothScrollToPosition(item, mRecyclerView));
            } else {
                mRecyclerView.smoothScrollToPosition(item);
            }
        }
    

    问题到这暂时变成了如何修改RecycleView 的滚动时长。还好这个问题网上是有一些方案的。基本都是继承LayoutManagere重写smoothScrollToPosition方法,开始滑动时设置一个自定义的RecyclerView.SmoothScroller对象,然后重写calculateSpeedPerPixel方法

        @Override
        public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
            LinearSmoothScroller smoothScroller =
                    new LinearSmoothScroller(recyclerView.getContext()) {
                        // 返回:滑过1px时经历的时间(ms)。
                        @Override
                        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                            return 150f / displayMetrics.densityDpi;
                        }
                    };
    
            smoothScroller.setTargetPosition(position);
            startSmoothScroll(smoothScroller);
        }
    
    

    calculateSpeedPerPixel的返回值则代表了RecycleView划过一个px所用的时间(这块有个小问题,后续讲述)。所以方案现在基本可以敲定就是自定义一个LinearLayoutManager,然后通过反射去替换viewpager2里的mLayoutManager字段。但是在执行时遇到一个问题,因为反射替换的时间肯定是viewpager2初始化之后,而viewpage2初始化之后其实是有很多其他字段已经持有了mLayoutManager的引用。

        private void initialize(Context context, AttributeSet attrs) {
            ...
            mLayoutManager = new LinearLayoutManagerImpl(context);
            mRecyclerView.setLayoutManager(mLayoutManager);
            ...
            mPageTransformerAdapter = new PageTransformerAdapter(mLayoutManager);
            .
        }
    

    所以反射替换不能只替换viewpager2的mLayoutManager字段,还需要替换持有mLayoutManager引用的对象中的相关字段。这样一来有可能陷入一个循环,不断的去替换关联对象中的LinearLayoutManager实例。所以这个时候这个思路已经不是很合适了。其实我们通过阅读viewpager2的源码可以发现setCurrentItem的调用栈是特别浅的,只有两层,而且代码量也不大,我们完全可以在我们自己的代码中模拟setCurrentItem的以及LayoutManager.smoothScrollToPosition的方法体,这样就能方便的替换mLayoutManager smoothScrollToPosition中的关键对象。整体方案还是比较简单的,下面贴上整个工具类的代码:

    class ViewPager2SlowScrollHelper(private val vp: ViewPager2, var duration: Long) {
        private val recyclerView: RecyclerView
        private val mAccessibilityProvider: Any
        private val mScrollEventAdapter: Any
        private val onSetNewCurrentItemMethod: Method
        private val getRelativeScrollPositionMethod: Method
        private val notifyProgrammaticScrollMethod: Method
    
        init {
            val mRecyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
            mRecyclerViewField.isAccessible = true
            recyclerView = mRecyclerViewField.get(vp) as RecyclerView
            recyclerView.layoutManager
            val mAccessibilityProviderField =
                ViewPager2::class.java.getDeclaredField("mAccessibilityProvider")
            mAccessibilityProviderField.isAccessible = true
            mAccessibilityProvider = mAccessibilityProviderField.get(vp)
            onSetNewCurrentItemMethod =
                mAccessibilityProvider.javaClass.getDeclaredMethod("onSetNewCurrentItem")
            onSetNewCurrentItemMethod.isAccessible = true
    
    
            val mScrollEventAdapterField =
                ViewPager2::class.java.getDeclaredField("mScrollEventAdapter")
            mScrollEventAdapterField.isAccessible = true
            mScrollEventAdapter = mScrollEventAdapterField.get(vp)
            getRelativeScrollPositionMethod =
                mScrollEventAdapter.javaClass.getDeclaredMethod("getRelativeScrollPosition")
            getRelativeScrollPositionMethod.isAccessible = true
    
            notifyProgrammaticScrollMethod = mScrollEventAdapter.javaClass.getDeclaredMethod(
                "notifyProgrammaticScroll",
                Int::class.java,
                Boolean::class.java
            )
            notifyProgrammaticScrollMethod.isAccessible = true
        }
    
        /**
         * 模拟手写Viewpage2的setCurrentItemInternal(int item, boolean smoothScroll)方法
         * 其中smoothScroll为true
         * 主要目的是通过手动实现vp的翻页方法达到控制RecycleView执行滚动的SmoothScroller对象
         */
        fun setCurrentItem(item: Int) {
            var item = item
            val adapter: RecyclerView.Adapter<*> = vp.adapter as RecyclerView.Adapter<*>
            if (adapter.itemCount <= 0) {
                return
            }
            item = item.coerceAtLeast(0)
            item = item.coerceAtMost(adapter.itemCount - 1)
            if (item == vp.currentItem && vp.scrollState == ViewPager2.SCROLL_STATE_IDLE) {
                return
            }
            if (item == vp.currentItem) {
                return
            }
            vp.currentItem = item
            onSetNewCurrentItemMethod.invoke(mAccessibilityProvider)
            notifyProgrammaticScrollMethod.invoke(mScrollEventAdapter, item, true)
            smoothScrollToPosition(item, vp.context, recyclerView.layoutManager)
        }
    
        /**
         * 模拟手写RecyclerView的smoothScrollToPosition方法 替换了startSmoothScroll的参数达到了改变速度的目的
         */
        private fun smoothScrollToPosition(
            item: Int,
            context: Context,
            layoutManager: RecyclerView.LayoutManager?
        ) {
            val linearSmoothScroller = getSlowLinearSmoothScroller(context)
            replaceDecelerateInterpolator(linearSmoothScroller)
            linearSmoothScroller.targetPosition = item
            layoutManager?.startSmoothScroll(linearSmoothScroller)
        }
    
        /**
         * 减速核心SmoothScroller对象,super.calculateSpeedPerPixel(displayMetrics) * slowCoefficient 为速度放慢slowCoefficient倍
         * 既动画时长增加slowCoefficient倍
         */
        private fun getSlowLinearSmoothScroller(context: Context): RecyclerView.SmoothScroller {
            return object : LinearSmoothScroller(context) {
                /**
                 * ??????
                 * ??????
                 * 按照sdk注释的内容理解这个方法的返回值为每个像素滚动的时间 例如返回 1 则代表滚动1个像素需要1ms 既1920px的滚动距离 则需要滚动1.92s
                 * 所以返回值应该是 duration/width 比如期望滚动1s 也就是需要返回 1000/vp.width
                 * 但是根据实际测试 如果按照返回值是 duration/width来计算  当返回 duration/width = 1时 duration期望应该是with(假设with是1920px duration则是1920ms)但是实际duration约等于3倍with(1920px滚动5700ms )????
                 * 暂无实际证据可以证实这个值是 3倍
                 * 但是calculateSpeedPerPixel的返回值的确和sdk注释描述的是有出入的,暂时先用3作为调整系数
                 * 也有可能是和我们设备相关 横屏 1920*1080 320dpi,使用的时候可以重新测试一下。
                 * ??????
                 * ??????
                 */
                override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
                    return duration/(vp.width.toFloat()*3.0f)
                }
            }
        }
    
        /**
         * 修改SmoothScroller的默认差值器,将其改为线性输出,不然会影响后续的vp动画
         * 如果没有自定义动画可以不用这个方法
         */
        private fun replaceDecelerateInterpolator(linearSmoothScroller: RecyclerView.SmoothScroller) {
            val mDecelerateInterpolatorField =
                LinearSmoothScroller::class.java.getDeclaredField("mDecelerateInterpolator")
            mDecelerateInterpolatorField.isAccessible = true
            mDecelerateInterpolatorField.set(linearSmoothScroller, object : DecelerateInterpolator() {
                override fun getInterpolation(input: Float): Float {
                    return input
                }
            })
        }
    }
    

    需要额外注意点的是calculateSpeedPerPixel方法,这个方法经过我的实际测试和sdk的注释描述并不相符,也可能是我的设备问题,大家使用的时候需要注意这个问题。

    使用方式是直接使用ViewPager2SlowScrollHelper.setCurrentItem 代替viewpager2.setCurrentItem 就可以了

    相关文章

      网友评论

          本文标题:ViewPager2修改翻页动画时间

          本文链接:https://www.haomeiwen.com/subject/blgvwltx.html