1、嵌套滚动机制简介
与嵌套滚动机制有关的接口或者辅助类主要有如下截图几个类
按照类别可以分为两大类别:NestedScrollingChildXXX、NestedScrollingParentXXX接口行为类别;NestedScrollingChildHelper、NestedScrollingParentHelper嵌套委派辅助类。官方这么设计,将嵌套滚动逻辑处理单独剥离,没有依附在具体的子View和父View,嵌套逻辑都抽离在NestedScrollingParentHelper、NestedScrollingChildHelper委派类里面。换句话说,Helper类封装了嵌套滚动传动逻辑。
嵌套滚动一般是由嵌套子View(也就是实现了NestedScrollingChildXXX系列接口的一方)发起的,父嵌套布局被动接受。结合事件拦截分发以及嵌套滚动时对应的接口流程,我将嵌套滚动链路大致梳理了如下几个步骤(嵌套子View假设是一个ViewGroup,实际使用过程中,这种场景占据绝大部分):
以子view child为例,以上嵌套流程里面的嵌套调用方法都是NestedScrollingChildXXX接口里面声明的方法,方法对应的具体执行逻辑都是委托给NestedScrollingChildHelper辅助类去实现的。
注意一个概念,嵌套并不需要要求直接父子View节点嵌套,中间可以隔着很多层,嵌套子View会去找最近的嵌套父View,这是什么意思呢?这里我们可以看下开始嵌套滚动时的源码入口startNestedScroll(int axes, int type),实际执行者是NestedScrollingChildHelper里面的startNestedScroll(axes, type)方法,贴出源码。
如图中源码所示,执行开始嵌套滚动的代码,会直接遍历到最近的具有嵌套滚动能力的父View。
2、WebView实现为嵌套子View
依据第一小节介绍的嵌套滚动机制,那么如何将一个view实现为嵌套子View的大致思路还是清晰的。先把最简单的流程环节先串完,那就是先实现NestedScrollingChildXXX接口,并将对应的调用方法委托给NestedScrollingChildHelper辅助类去调用自己同签名方法去执行。这一步大部分人应该都没有问题。下面就是如何在事件分发事件流程里面,如何控制嵌套流程以及处理Parent与Child在边界处的嵌套事件传递与嵌套事件消耗问题。
这里其实存在一定的经验技巧,因为google本身提供了一些嵌套子View API,比如RecyclerView、NestedScrollView。这些API其实就是很好的源码实现参考对象。需要注意的地方就是,虽然这些API都实现了嵌套滚动机制,但是要考虑到不同的View本身的特性,对于细节需要个性化处理(这里有个前提,就是对参考的API源码整体有个把控,尽量选取源码比较清晰,结构简单的去参考,作者本人经过阅读,选取了更为简单的NestedScrollView源码实现作为参考)。当然,自己理清思路去处理细节也是可以的(头铁的大神们,膜拜)。
言归正传,那么WebView具体如何实现嵌套滚动呢?
2.1、实现嵌套接口,并委托给NestedScrollingChildHelper
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type, consumed);
}
// NestedScrollingChild2
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll(int type) {
mChildHelper.stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent(int type) {
return mChildHelper.hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
}
@Override
public void stopNestedScroll() {
stopNestedScroll(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean hasNestedScrollingParent() {
return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
2.2、嵌套滚动行为位置、嵌套滚动记录、滚动行为需要的变量定义
private OverScroller mScroller;//滚动
private int mTouchSlop;//判断当前move是否为拖动的判断条件
private int mMinimumVelocity;//判断ACTION_UP后,当前是否应该执行fling的下限阈值
private int mMaximumVelocity;//执行fling操作时,最大的速率阈值
private boolean mIsBeingDragged = false;//标记当前行为是否为拖动行为
private VelocityTracker mVelocityTracker;//速度追踪器
/**
* ID of the active pointer. This is used to retain consistency during
* drags/flings if multiple pointers are used.
*/
private int mActivePointerId = INVALID_POINTER;//这个用来保存多个手指交互操作的场景,多指交互也是个蛮有研究意义的点,感兴趣的可以深入了解下
private static final int INVALID_POINTER = -1;
/**
* Position of the last motion event.
*/
private int mLastMotionY;
/**
* Used during scrolling to retrieve the new offset within the window.
*/
private final int[] mScrollOffset = new int[2];
private final int[] mScrollConsumed = new int[2];
private int mNestedYOffset;//用来修正嵌套滚动过程中Y坐标方向的实际位置
private int mLastScrollerY;
2.3、onTouch中如何实现嵌套流程,并不影响原有webView的特性
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
MotionEvent vtev = MotionEvent.obtain(ev);
vtev.offsetLocation(0, mNestedYOffset);//修正Y方向实际位置
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
abortAnimatedScroll();
}
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);//开始嵌套滚动
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
//即将开始嵌套滚动
deltaY -= mScrollConsumed[1];
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
}
if (mIsBeingDragged) {
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
mScrollConsumed[1] = 0;
dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH, mScrollConsumed);//开始嵌套滚动,手指没有松开状态的
mLastMotionY -= mScrollOffset[1];
mNestedYOffset += mScrollOffset[1];
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
if (!dispatchNestedPreFling(0, -initialVelocity)) {//提前判断父View是否消耗完所有的fling消耗量,没有的话,剩余消耗量由子View去执行
dispatchNestedFling(0, -initialVelocity, true);//fling操作通知给父view,父view决定当前场景是否消耗
fling(-initialVelocity);//子View执行fling
}
}
mActivePointerId = INVALID_POINTER;
endDrag();//结束嵌套滚动,重置资源
break;
//..........
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return super.onTouchEvent(ev);//这里需要注意,onTouch里面具体是否消耗事件,不要屏蔽webView
自身的逻辑,不然会影响webView本身内部的事件分发切换。
}
针对嵌套滚动过程中fling的操作,具体的细节在computeScroll方法里面。
@Override
public void computeScroll() {
super.computeScroll();//这个方法不要去掉,保留webView原有的滚动执行逻辑
if (mScroller.isFinished()) {
return;
}
mScroller.computeScrollOffset();
final int y = mScroller.getCurrY();
int unconsumed = y - mLastScrollerY;
mLastScrollerY = y;
// Nested Scrolling Pre Pass
mScrollConsumed[1] = 0;
dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null,
ViewCompat.TYPE_NON_TOUCH);//在fling的过程中,将未消耗完的消耗量交给TYPE_NON_TOUCH类型的嵌套滚动处理,这里是预处理,提前计算,这里还没有开始滚动
unconsumed -= mScrollConsumed[1];
final int range = getScrollRange();
if (unconsumed != 0) {
// Internal Scroll
final int oldScrollY = getScrollY();
overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false);
final int scrolledByMe = getScrollY() - oldScrollY;
unconsumed -= scrolledByMe;
// Nested Scrolling Post Pass
mScrollConsumed[1] = 0;
dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset,
ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);//在fling的过程中,将未消耗完的消耗量交给TYPE_NON_TOUCH类型的嵌套滚动处理,这里是开始滚动
unconsumed -= mScrollConsumed[1];
}
if (unconsumed != 0) {//子view还是没有消耗完,停止子View的滚动操作
abortAnimatedScroll();
}
if (!mScroller.isFinished()) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
网友评论