解决CoordinatorLayout中RecyclerView抖动问题

前言

当我们使用CoordinatorLayout + AppBarLayout  + RecyclerView 实现滑动效果时,当手指在 AppBarLayout 上快速向下滑动,然后迅速在 RecyclerView 

向上滑动,这个时候你会看到界面先向下滑动一下,然后颤抖几下,又上上滑动了,也就是本文要解决的滑动抖动问题,而且我发现很多App都有这个问题,

只是当 AppBarLayout 的高度小时不明显,当 AppBarLayout 很高后,用户就会很容易发现这个问题。

1. 问题分析

我们知道 CoordinatorLayout 之所以能够实现很多滚动动画效果,是因为有了 Behavior ,而 Behavior 是用来处理滚动事件的。

它里面有一套 xxNestedScrollxx 大法,看着着实令人头疼,说明文档少,操作过于复杂,而且已经升级过几次了,可以看到有很多 API 都是 Deprecated 状态了。

而我们的RecyclerView的 behavior 也是实现了NestedScroll接口的。

既然是滚动出了问题,而 behavior 就是处理各种滑动事件的,所有我们需要从 behavior 入手解决。

通过测试我们发现,是在我们手势fling的时候才会出现问题,而缓慢滑动是没有问题的。并且是双向滑动都会出现问题,

即从 AppBarLayout -> RecyclerView 和从 RecyclerView -> AppBarLayout 滑动都有问题。

也就是我们需要处理 AppBarLayout 和 RecyclerView 的 behavior 事件。

下面上代码,忘记了是从哪个博主那看到的了,有人看到知道了可以告诉我下,我好附上链接,谢谢。

class AppBarLayoutBehavior(context: Context, attrs: AttributeSet?) : AppBarLayout.Behavior(context, attrs) {

    private var isFlinging = false
    private var shouldBlockNestedScroll = false

    override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: AppBarLayout, ev: MotionEvent): Boolean {
        shouldBlockNestedScroll = isFlinging
        when (ev.actionMasked) {
            MotionEvent.ACTION_DOWN -> // 手指触摸屏幕的时候停止fling事件
                stopAppbarLayoutFling(child)
            else -> {
            }
        }
        return super.onInterceptTouchEvent(parent, child, ev)
    }

    /**
     * 反射获取私有的flingRunnable 属性,考虑support 28以后变量名修改的问题
     * @return Field
     * @throws NoSuchFieldException
     */
    private val flingRunnableField: Field?
        get() {
            val superclass = this.javaClass.superclass
            return try { // support design 27及一下版本
                val headerBehaviorType = superclass?.superclass
                headerBehaviorType?.getDeclaredField("mFlingRunnable")
            } catch (e: NoSuchFieldException) {
                e.printStackTrace()
                // 可能是28及以上版本
                val headerBehaviorType = superclass?.superclass?.superclass
                headerBehaviorType?.getDeclaredField("flingRunnable")
            }
        }

    /**
     * 反射获取私有的scroller 属性,考虑support 28以后变量名修改的问题
     * @return Field
     * @throws NoSuchFieldException
     */
    @get:Throws(NoSuchFieldException::class)
    private val scrollerField: Field?
        get() {
            val superclass = this.javaClass.superclass
            return try { // support design 27及一下版本
                val headerBehaviorType = superclass?.superclass
                headerBehaviorType?.getDeclaredField("mScroller")
            } catch (e: NoSuchFieldException) {
                e.printStackTrace()
                // 可能是28及以上版本
                val headerBehaviorType = superclass?.superclass?.superclass
                headerBehaviorType?.getDeclaredField("scroller")
            }
        }

    /**
     * 停止appbarLayout的fling事件
     * @param appBarLayout
     */
    private fun stopAppbarLayoutFling(appBarLayout: AppBarLayout) {
        try {
            val flingRunnableField = flingRunnableField
            flingRunnableField?.isAccessible = true
            val flingRunnable = flingRunnableField?.get(this) as? Runnable
            flingRunnable?.let {
                appBarLayout.removeCallbacks(it)
                flingRunnableField.set(this, null)
            }
            val scrollerField = scrollerField
            scrollerField?.isAccessible = true
            val overScroller = scrollerField?.get(this) as? OverScroller
            if (overScroller != null && !overScroller.isFinished) {
                overScroller.abortAnimation()
            }
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
    }

    override fun onStartNestedScroll(
        parent: CoordinatorLayout,
        child: AppBarLayout,
        directTargetChild: View,
        target: View,
        nestedScrollAxes: Int,
        type: Int
    ): Boolean {
        stopAppbarLayoutFling(child)
        return super.onStartNestedScroll(parent, child, directTargetChild, target,
                nestedScrollAxes, type)
    }

    override fun onNestedPreScroll(
        coordinatorLayout: CoordinatorLayout,
        child: AppBarLayout,
        target: View,
        dx: Int,
        dy: Int,
        consumed: IntArray,
        type: Int
    ) {
        if (type == ViewCompat.TYPE_NON_TOUCH) {
            isFlinging = true
        }
        if (!shouldBlockNestedScroll) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
        }
    }

    override fun onNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: AppBarLayout,
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int
    ) {
        if (!shouldBlockNestedScroll) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed,
                    dyConsumed, dxUnconsumed, dyUnconsumed, type)
        }
    }

    override fun onStopNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        abl: AppBarLayout,
        target: View,
        type: Int
    ) {
        super.onStopNestedScroll(coordinatorLayout, abl, target, type)
        isFlinging = false
        shouldBlockNestedScroll = false
    }
}

但是,上面只解决了其中一个方向 AppBarLayout -> RecyclerView 的问题,上面已经说了是两个方向都有问题,下面是另外一个方向的:

class RecyclerViewBehavior(context: Context?, attrs: AttributeSet?) : AppBarLayout.ScrollingViewBehavior(context, attrs) {

    private var mRecyclerView: RecyclerView? = null

    override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: View, ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                if (!onTouchedSelf(child, ev)) {
                    (mRecyclerView ?: getNestedScrollingChild(child))?.let {
                        it.stopNestedScroll()
                        it.stopScroll()
                        mRecyclerView = it
                    }
                }
            }
        }
        return super.onInterceptTouchEvent(parent, child, ev)
    }

    private fun onTouchedSelf(view: View, ev: MotionEvent): Boolean {
        val selfRect = IntArray(2)
        view.getLocationOnScreen(selfRect)
        return ev.rawY > selfRect[1]
    }

    private fun getNestedScrollingChild(parentView: View?): RecyclerView? {
        when (parentView) {
            is RecyclerView -> {
                return parentView
            }
            is ViewGroup -> {
                for (index in 0 until parentView.childCount) {
                    (parentView.getChildAt(index) as? ViewGroup)?.let {
                        getNestedScrollingChild(it)?.let { recyclerView ->
                            return recyclerView
                        }
                    }
                }
                return null
            }
            else -> {
                return null
            }
        }
    }
}

 

相关链接:https://www.jianshu.com/p/b987fad8fcb4?tdsourcetag=s_pcqq_aiomsg

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值