综述
上图是一个非常常见的嵌套滑动UI交互,实现这样的效果,大致有如下三种思路:
基于普通的事件分发机制
基于NestedScrolling机制
基于CoordinatorLayout与Behavior
以上三种思路从原理上循序渐进,逐层封装。由于本文主要介绍嵌套滑动,会主要介绍第二种方案及其原理,第一种会稍微讲解一下。
Demo布局
<com.threeloe.nestscrolling.nest.ScrollHeaderLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/scrollHeaderLayout"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="Header"/>
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</com.threeloe.nestscrolling.nest.ScrollHeaderLayout>
无论采用哪种实现方式,布局都分为上部分的Header和下部分的ViewPager两部分。
传统的事件分发机制
优点:
灵活性最高。
缺点:
处理细节多,难度大,需要对事件分发机制, 多点触控,滑动,fling,以及一些周边API等都比较清楚。
基本思路:
要完成上述效果,在竖直滑动的情况下,上滑时先让外层的父View滚动,到滚动的最大距离时候,再让子View开始滚动。下滑时如果子View滑动距离不是0的话,先让子View滑动,然后让父View滑动。因此一次滑动中的事件需要再父View和子View中切换传递。
复习一下事件分发机制:
事件序列:从手指按下知道抬起,中间经历一个ACTION_DONW ,多个ACTION_MOVE和一个ACTION_UP
一般情况下我们处理滑动冲突,重写onInterceptTouchEvent方法即可,但是一旦onInterceptTouchEvent方法返回true,那么该事件序列以后的事件都会直接给父View处理,这种情况在处理滑动冲突是是可行的。但是在我们上面的案例因为对于一个事件序列需要交替得在子View和父View中传递,如果重写该方法的话,需要我们自己再合适时机手动派发一些事件。
因此更为简单的做法不如直接重写dispatchTouchEvent方法,以下代码只是处理了单手指滑动的情况,没有考虑多点触控,也没有处理fling。
如上我们需要判断isInnerScrollViewTop(),即内部的View滑动距离是否为0。因此父View需要知道滑动的子View到底是谁,需要外界告诉。
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
mLastX = ev.x
mLastY = ev.y
}
MotionEvent.ACTION_MOVE -> {
if (!mIsReadyToDragHorizontal) {
var dy = (mLastY - ev.y).toInt()
var dx = (mLastX - ev.x).toInt()
//当连续滑动距离达到TouchSlop时候,认为滑动
if (!mIsBeingDragged) {
if (Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop
} else {
dy += mTouchSlop
}
mIsBeingDragged = true
}
if (Math.abs(dx) > mTouchSlop) {
when {
dy == 0 -> mIsReadyToDragHorizontal = true
Math.abs(dx).toFloat() / Math.abs(dy).toFloat() > 30 -> mIsReadyToDragHorizontal = true
else -> {
mIsBeingDragged = true
if (dy > 0) {
dy -= mTouchSlop
} else {
dy += mTouchSlop
}
}
}
}
}
if (mIsBeingDragged) {
mLastY = ev.y
var consumedDy = 0
if (dy == 0) {
//过滤掉
return true
} else if (dy > 0) {
consumedDy = Math.min(dy, mScrollRange - scrollY)
} else {
if (isInnerScrollViewTop()) {
consumedDy = Math.max(dy, -scrollY)
}
}
if (consumedDy != 0) {
scrollBy(0, consumedDy)
}
}
}
}
MotionEvent.ACTION_UP -> {
mIsBeingDragged = false
mIsReadyToDragHorizontal = false
}
}
//?
super.dispatchTouchEvent(ev)
return true
}
NestedScrolling机制
Android 5.0之后加入该机制。
support v4包提供两个接口:
NestedScrollingParent,嵌套滑动的父View需要实现。已有实现CoordinatorLayout,NestedScroView
NestedScrollingChild, 嵌套滑动的子View需要实现。已有实现RecyclerView,NestedScroView
Google在给我提供这两个接口的时候,同时也给我们提供了实现这两个接口时一些方法的标准实现,
分别是
NestedScrollingChildHelper
NestedScrollingParentHelper
我们在实现上面两个接口的方法时,只需要调用相应Helper中相同签名的方法即可。
之后由于NestedScrollingParent/NestedScrollingChild功能有些不足,Google又引入NestedScrollingParent2/NestedScrollingChild2,具体引入原因下文会说。
本文示例代码主要是NestedScrollingParent2/NestedScrollingChild2
基本原理
对原始的事件分发机制做了一层封装,子View实现NestedScrollingChild接口,父View实现NestedScrollingParent 接口。
假设产生一个竖直滑动,简单来说滑动事件会由NestedScrollingChild先接收到产生一个dy,然后询问NestedScrollingParent要消耗多少(dyConsumed),自己再拿dy-dyConsumed来进行滑动。当然NestedScrollingChild有可能自己本身也并不会消耗完,此时会再向父View报告情况。
NestedScrollingParent
NestedScrollingParentHelper 只为我们提供了onNestedScrollAccepted,onStopNestedScroll,getNestedScrollAxes三个方法的实现,其余的方法我们根据自身需要自己实现。NestedScrollingParent的方法基本上都是提供给NestedScrollingChild来调用的,我们自己无需调用。
本例使用的是27.0.0的RecyclerView,实现了NestedScrollingChild2,下面是本例中NestedScrollingParent2的完整实现。
class ScrollHeaderLayout : LinearLayout, NestedScrollingParent2 {
private lateinit var mNestedScrollingParentHelper: NestedScrollingParentHelper
private lateinit var mHeaderView: View
private lateinit var mBottomView: View
private var mScrollRange = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
orientation = VERTICAL
mNestedScrollingParentHelper = NestedScrollingParentHelper(this)
}
override fun onFinishInflate() {
super.onFinishInflate()
if (childCount != 2) {
throw IllegalStateException("ScrollHeaderLayout must have two children")
}
mHeaderView = getChildAt(0)
mBottomView = getChildAt(1)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
mScrollRange = scrollEvaluator.getScrollRange(mHeaderView)
val bottomHeightSpec = MeasureSpec.makeMeasureSpec(measuredHeight - mHeaderView.measuredHeight + mScrollRange, MeasureSpec.EXACTLY)
measureChild(mBottomView, widthMeasureSpec, bottomHeightSpec)
}
/**
* -----------------------------------------------------------
* NestedScrollingParent
*/
/**
* NestedScrollingChild 未fling之前告诉准备fling的情况
*
* @param target 具体嵌套滑动的那个子类
* @param velocityX 水平方向速度
* @param velocityY 垂直方向速度
* @return true 父View是否消耗了fling
*/
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
return false
}
/**
* NestedScrollingChild 在fling之后告诉自己fling情况
*
* @param target 具体嵌套滑动的那个子类
* @param velocityX 水平方向速度
* @param velocityY 垂直方向速度
* @param consumed 子view是否fling了
* @return true 父View是否消耗了fling
*/
override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
return false
}
/**
* -----------------------------------------------------------
* NestedScrollingParent2
*/
/**
* 有子View发起了嵌套滑动,确认该父View是否接受嵌套滑动
*
* @param child target向上一直寻找NestedScrollingParent,child在这个路径上,是NestedScrollingParent的直接子View
* @param target NestedScrollingChild,即发起NestedScrolling的类
* @param axes 嵌套滑动的方向,水平方向,垂直方向,或者不指定
* @param type
* @return 是否接受该嵌套滑动
*/
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL
}
/**
* 表示该父View已经接受了嵌套滑动。onStartNestedScroll 方法返回true后该方法会调用。
* NestedScrollingParentHelper为我们提供了该方法的标准实现。
*
*/
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
}
/**
* NestedScrollingChild在准备滑动前先询问NestedScrollingParent需要消耗多少
*
* @param dx NestedScrollingChild水平方向想要滚动的距离
* @param dy 垂直方向嵌套滑动的子View竖直方向想要滚动的距离
* @param consumed 这个参数用于告诉NestedScrollingChild 父View消耗掉的距离
* consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
*/
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray?, type: Int) {
val headViewHeight = mScrollRange
var consumedDy = 0
if (dy > 0) {
consumedDy = Math.min(dy, headViewHeight - scrollY)
} else {
if (target is RecyclerView) {
if (ScrollHelper.isRecyclerViewTop(target)) {
consumedDy = Math.max(dy, -scrollY)
}
}
}
consumed?.set(1, consumedDy)
scrollBy(0, consumedDy)
}
/**
* NestedScrollingChild自身也不一定消耗完全部距离,因此
* NestedScrollingChild自身滑动完成后,告诉NestedScrollingParent自己的滑动情况
* @param dxConsumed NestedScrollingChild水平方向消耗的距离
* @param dyConsumed NestedScrollingChild竖直方向消耗的距离
* @param dxUnconsumed NestedScrollingChild水平方向未消耗的距离
* @param dyUnconsumed NestedScrollingChild竖直方向未消耗的距离
*/
override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
Log.i(ScrollHeaderLayout::class.java.simpleName, "dyConsumed:$dyConsumed,dyUnconsumed:$dyUnconsumed")
}
/**
* 停止嵌套滑动时
*/
override fun onStopNestedScroll(target: View, type: Int) {
mNestedScrollingParentHelper.onStopNestedScroll(target, type)
}
/**
* ------------------------------------
*/
private var scrollEvaluator: ScrollRangeEvaluator = object : ScrollRangeEvaluator {
override fun getScrollRange(header: View): Int {
return if ((header is ViewGroup) && header.childCount > 0) {
header.getChildAt(0).measuredHeight
} else {
header.measuredHeight
}
}
}
fun setScrollRangeEvaluator(evaluator: ScrollRangeEvaluator) {
this.scrollEvaluator = evaluator
}
interface ScrollRangeEvaluator {
fun getScrollRange(header: View): Int
}
}
NestedScrollingChild
一般情况下我们并不需要自己实现一个NestedScrollingChild, 系统已经为我们提供了RecyclerView和NestedScrollView大多数情况下都够用了,这里只是帮助大家更好理解它。
我们自己要实现一个NestedScrollingChild分为两步
1) 实现NestedScrollingChild里的方法。这一步非常简单,NestedScrollingChildHelper里面已经为我们提供了所有NestedScrollingChild所需要的实现。
2)在合适的实际调用相应的方法,大部分都需要在onTouchEvent方法中调用。调用时机下文会以RecyclerView为例来讲解。
class NestedChildView(context: Context, attrs: AttributeSet?) : View(context, attrs), NestedScrollingChild2 {
private var mScrollingChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
init {
isNestedScrollingEnabled = true
}
/**
* 设置是否开启嵌套滑动
* @param enabled
*/
override fun setNestedScrollingEnabled(enabled: Boolean) {
mScrollingChildHelper.isNestedScrollingEnabled = enabled
}
override fun isNestedScrollingEnabled(): Boolean {
return mScrollingChildHelper.isNestedScrollingEnabled
}
/**
* 开始嵌套滑动流程,一般ACTION_DOWN里面调用。
* 调用这个函数的时候会向上寻找NestedScrollingParent,如果找到了并且NestedScrollingParent 说可以滑动的话就返回true,否则返回false
* @param axes:支持嵌套滚动轴。水平方向,垂直方向,或者不指定
* @return true 父控件说可以滑动,false 父控件说不可以滑动
*/
override fun startNestedScroll(axes: Int, type: Int): Boolean {
return mScrollingChildHelper.startNestedScroll(axes, type)
}
/**
* 是否有嵌套滑动对应的父控件
*/
override fun hasNestedScrollingParent(type: Int): Boolean {
return mScrollingChildHelper.hasNestedScrollingParent(type)
}
/**
* 在嵌套滑动的子View滑动之前,告诉父View滑动的距离,让父View做相应的处理。
*
* @param dx 告诉父View水平方向需要滑动的距离
* @param dy 告诉父View垂直方向需要滑动的距离
* @param consumed 出参. 父View通过这个参数告诉子View,自己对事件的消耗情况。consumed[0]父View告诉子View水平方向滑动的距离(dx)
* consumed[1]父View告诉子View垂直方向滑动的距离(dy).
* @param offsetInWindow 可选 length=2的数组,如果父View滑动导致子View的窗口发生了变化(子View的位置发生了变化)
* 该参数返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的变化。 这个参数用于对触摸事件位置进行校准。
* 如果你记录了手指最后的位置,需要根据参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
*
* 一般在ACTION_MOVE中准备滑动之前
* @return true 父View滑动了,false 父View没有滑动。
*/
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?,type: Int): Boolean {
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,type)
}
/**
* 在嵌套滑动的子View滑动之后再调用该函数向父View汇报滑动情况。
*
* @param dxConsumed 子View水平方向滑动的距离
* @param dyConsumed 子View垂直方向滑动的距离
* @param dxUnconsumed 子View水平方向没有滑动的距离
* @param dyUnconsumed 子View垂直方向没有滑动的距离
*
* 一般在在ACTION_MOVE中调用,在dispatchNestedPreScroll之后
* @return true 如果父View有滑动做了相应的处理, false 父View没有滑动.
*/
override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
offsetInWindow: IntArray?,type: Int): Boolean {
return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow,type)
}
/**
* 停止嵌套滑动流程(一般ACTION_UP里面调用)
*/
override fun stopNestedScroll(type: Int) {
mScrollingChildHelper.stopNestedScroll()
}
/**
* 在嵌套滑动的子View fling之前告诉父View fling的情况。
*
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @return 如果父View fling了
*/
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY)
}
/**
* 在嵌套滑动的子View fling之后再调用该函数向父View汇报fling情况。
*
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @param consumed true 如果子View fling了, false 如果子View没有fling
* @return true 如果父View fling了
*/
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mScrollingChildHelper.onDetachedFromWindow()
}
}
Why V2
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
return false
}
override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
return false
}
NestedScrollingParent中为我们提供了如上两个方法用于处理fling事件,但是由于传过来一个速度。对于速度而言无法说父View消耗一部分,子View消耗一部分。因此老版本fling事件只能由父View或者子View中的一个处理。这种情况显然不合理,比如示例Demo滑动速度大,父View滑动完,子View应该继续滑动。
针对fling无法在子View和父View之间交替的问题,NestedScrollingParent2直接废弃onNestedPreFling和onNestedFling方法。 并给原来的onStartNestedScroll,onNestedScrollAccepted,onNestedPreScroll,onNestedScroll,onStopNestedScroll方法添加一个type参数,定义如下
@IntDef({TYPE_TOUCH, TYPE_NON_TOUCH})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP)
public @interface NestedScrollType {}
TYPE_TOUCH表示正常的手指触摸的滚动
TYPE_NON_TOUCH表示的是fling引起的滚动
然后再fling时候也会重新走一遍嵌套滑动的流程,只是type传的TYPE_NON_TOUCH。
源码分析
以RecyclerView为例分析,RecylerView实现NestedScrollingParent2接口,方法的实现和NestedChildView几乎一样,我们主要是看一下相应方法的调用时机,以及NestedScrollingChildHelper的标准实现做了些什么。
@Override
public boolean onTouchEvent(MotionEvent e) {
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
//如果父View发生了滑动等,触摸事件位置需要偏移
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//1.ACTION_DOWN时候开始嵌套滑动
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
//2.RecylcerView没开始滑动,先问一下父View是不是需要滑动
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
//减去父View消耗
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// 父View滑动的话更新offset
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//3.自身滑动,并向父View报告滑动情况
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
//fling触发调用
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
//4.停止嵌套滑动
resetTouch();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
- ACTION_DOWN时候开始嵌套滑动
startNestedScroll的目的就是向上找到NestedScrollParent并询问是否接要嵌套滑动
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//循环往上寻找NestedScrollingParent
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
//为什么判断
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
如果是NestedScrollingParent2的话直接onStartNestedScroll,不是的话因为之前老版本的NestedScrollingParent只支持TYPE_TOUCH的滑动,因此需要判断一下。
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
return false;
}
记录找到的NestedScrollingParent。
private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
switch (type) {
case TYPE_TOUCH:
mNestedScrollingParentTouch = p;
break;
case TYPE_NON_TOUCH:
mNestedScrollingParentNonTouch = p;
break;
}
}
- ACTION_MOVE,子View未开始滑动前先询问父View是否消耗
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
//获取找打startNestedScroll时候找到的NestedScrollingParent
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
//记录RecyclerView在滑动事件传给父View前 在窗口上位置
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
//置0
consumed[0] = 0;
consumed[1] = 0;
//调用NestedScrollingParent的onNestedPreScroll
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
//父View滑动后位置减去滑动前位置得到一个偏移量
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//通过consumed!=0确定父View是否消耗
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
3.NestedScrollingChild完成对滚动事件的消耗,并向NestedScrollingParent报告
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
if (mAdapter != null) {
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
//RecylerView滑动,返回自己滑动消耗的
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
//获取未消耗的
unconsumedY = y - consumedY;
}
}
//自己滑动消耗完事件后,向NestedScrollingParent报告自己滑动的情况,父View此时还可以进行一些滑动操作等
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
return consumedX != 0 || consumedY != 0;
}
dispatchNestedScroll的核心就是调用父View的onNestedScroll,代码很简单
- 停止嵌套滑动
ACTION_UP或者ACTION_CANCEL触发后,都会调用resetTouch这个方法。
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll(TYPE_TOUCH);
releaseGlows();
}
调用NestedScrollingParent的onStopNestedScroll方法,把自己的成员变量置空。
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}
- fling
public boolean fling(int velocityX, int velocityY) {
//这两个方法v2的版本其实不需要了,这里只是兼容一下
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//1.开始嵌套滑动
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
//ViewFlinger真正实现fling
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
class ViewFlinger implements Runnable {
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
//简单认为View.post
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
@Override
public void run() {
final OverScroller scroller = mScroller;
final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
if (scroller.computeScrollOffset()) {
final int[] scrollConsumed = mScrollConsumed;
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int dx = x - mLastFlingX;
int dy = y - mLastFlingY;
int hresult = 0;
int vresult = 0;
mLastFlingX = x;
mLastFlingY = y;
int overscrollX = 0, overscrollY = 0;
//2.调用dispatchNestedPreScroll
if (dispatchNestedPreScroll(dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {
dx -= scrollConsumed[0];
dy -= scrollConsumed[1];
}
if (mAdapter != null) {
if (dx != 0) {
hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
}
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
}
if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null,
TYPE_NON_TOUCH)
&& (overscrollX != 0 || overscrollY != 0)) {
final int vel = (int) scroller.getCurrVelocity();
int velX = 0;
if (overscrollX != x) {
velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
}
int velY = 0;
if (overscrollY != y) {
velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
}
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
absorbGlows(velX, velY);
}
if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
&& (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
scroller.abortAnimation();
}
}
if (hresult != 0 || vresult != 0) {
dispatchOnScrolled(hresult, vresult);
}
if (!awakenScrollBars()) {
invalidate();
}
final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically()
&& vresult == dy;
final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally()
&& hresult == dx;
final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal
|| fullyConsumedVertical;
//如果滑动完成了
if (scroller.isFinished() || (!fullyConsumedAny
&& !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
setScrollState(SCROLL_STATE_IDLE);
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
//停止嵌套滑动
stopNestedScroll(TYPE_NON_TOUCH);
} else {
//滑动没有完成,继续post执行run方法
postOnAnimation();
}
}
}
}