ViewPager2修改翻页动画时间

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容