总述
上图是一个非常常见的嵌套滑动UI交互,完结这样的效果,大致有如下三种思路: 1.依据普通的事情分发机制
2.依据NestedScrolling机制
3.依据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();
}
}
}
}