前言
当我们使用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