ViewPager2修改翻页动画时间

2021-09-23  本文已影响0人  FENGAO

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 就可以了

上一篇下一篇

猜你喜欢

热点阅读