上篇文章 Android嵌套滑动上篇 中实现的滑动效果并不是很流畅,如上图所示。不流畅的原因是因为对于fling,NestedScrolling
要么交给child处理,要么交给parent处理。
而通过NestedScrolling2
可以实现fling类型的滚动先由外层控件处理一部分,剩余的再交给内层控件处理,这样使滑动效果比较流畅。实现的效果如下图所示:
这里先说一下NestedScrolling2能让内层控件和外层控件在惯性滑动的时候更流畅的关键的逻辑。
-
NestedScrolling2分发滚动事件的时候区分了滚动事件的类型:是正常的触摸滚动还是惯性滑动。
-
内层控件先调用
dispatchNestedPreFling
来处理惯性滑动。如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling
方法返回了true。那么NestedScrolling2和NestedScrolling的惯性滑动效果没有什么差异。 -
如果外层控件没有处理惯性滑动,也就是外层控件的
onNestedPreFling
方法返回了false。那么就会调用dispatchNestedFling
方法并且内层控件自身开始惯性滑动mViewFlinger.fling(velocityX, velocityY)
,但是在惯性滑动的每一帧,通过Scroller计算出来的滚动距离通过dispatchNestedPreScroll
先分发给外层控件。外层控件可以通过onNestedPreScroll
先消耗部分滚动距离,然后内层控件再自身滚动。
流程图
NestedScrolling2流程.jpg接下来先看一下NestedScrolling2相关的几个类。
NestedScrollingChild2继承了NestedScrollingChild接口并新增了几个方法。
public interface NestedScrollingChild2 extends NestedScrollingChild {
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
void stopNestedScroll(@NestedScrollType int type);
boolean hasNestedScrollingParent(@NestedScrollType int type);
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type);
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type);
}
新增的几个方法都有一个@NestedScrollType int type
参数,是用来区分滚动类型的,有两个取值:
//触摸屏幕类型
public static final int TYPE_TOUCH = 0;
//惯性滑动类型
public static final int TYPE_NON_TOUCH = 1;
对应的NestedScrollingParent2继承了NestedScrollingParent接口也新增了几个方法。
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(View child, View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onNestedScrollAccepted(View child, View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onStopNestedScroll(View target, @NestedScrollType int type);
void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
void onNestedPreScroll(View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type);
}
最新的嵌套滑动已经都出NestedScrolling3了,这个更新也是日新月异啊,哈哈。
public interface NestedScrollingChild3 extends NestedScrollingChild2 {
void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
@Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
@NonNull int[] consumed);
}
public interface NestedScrollingParent3 extends NestedScrollingParent2 {
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
}
NestedScrolling3相对于NestedScrolling2的改变与本篇文章内容无关,我们暂时忽略。
下面我们以RecyclerView为例开始分析,androidx1.1.0
的源码。
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
//...
}
RecyclerView的onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent e) {
//...
final MotionEvent vtev = MotionEvent.obtain(e);
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
//...
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//注释1处
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_MOVE: {
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;
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//注释2处
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
//注释3处
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
//...
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//注释4处
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
} 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;
//注释5处,调用fling方法
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
//注释6处
resetScroll();
} break;
}
//...
return true;
}
注释1处,调用startNestedScroll方法
@Override
public boolean startNestedScroll(int axes, int type) {
//调用NestedScrollingChildHelper的startNestedScroll方法
return getScrollingChildHelper().startNestedScroll(axes, type);
}
这里提一下,使用NestedScrollingChildHelper和NestedScrollingParentHelper是为了对Android 5.0 Lollipop (API 21)
以前的版本做兼容。
NestedScrollingChildHelper的startNestedScroll方法
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
//根据嵌套滑动的类型来获取NestedScrollingParent
if (hasNestedScrollingParent(type)) {
// 嵌套滑动已经在处理过程中,直接返回true
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//遍历父级控件
while (p != null) {
//注释1处
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
//注释2处
setNestedScrollingParentForType(type, p);
//注释3处
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
如果开启了嵌套滑动就遍历父控件,询问是否有父控件想要处理。
注释1处,ViewParentCompat的onStartNestedScroll方法
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
//注释1处,首先尝试调用NestedScrollingParent2的API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {//注释2处,NestedScrollingParent只处理正常的触摸类型
if (Build.VERSION.SDK_INT >= 21) {//大于21版本直接调用ViewParent的方法即可。
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
//NestedScrollingParent处理
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
注释1处,首先尝试调用NestedScrollingParent2的onStartNestedScroll(child, target,nestedScrollAxes, type)
。
注释2处,NestedScrollingParent只处理正常的触摸类型,丢弃掉type参数,onStartNestedScroll(child, target,nestedScrollAxes)
。大于21版本直接调用ViewParent的方法即可。否则调用NestedScrollingParent处理。
后面的分析中,我们就只看NestedScrollingParent2相关的内容。
回到NestedScrollingChildHelper的startNestedScroll方法的注释2处。
private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
switch (type) {
case TYPE_TOUCH:
//处理触摸类型滑动事件的外层控件
mNestedScrollingParentTouch = p;
break;
case TYPE_NON_TOUCH:
//处理惯性滑动类型事件的外层控件
mNestedScrollingParentNonTouch = p;
break;
}
}
根据滑动的类型将ViewParent赋值给不同的变量保存。
注释3处
//注释3处
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
内部会调用NestedScrollingParent2的onNestedScrollAccepted方法。
回到RecyclerView的onTouchEvent方法的注释2处
//注释2处
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
//注释3处
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
//...
}
注释2处,RecyclerView的dispatchNestedPreScroll方法
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
最终会调用NestedScrollingParent2的onNestedPreScroll方法。
注释3处,如果NestedScrollingParent2消耗了一些滑动距离,减去消耗的距离。
RecyclerView的onTouchEvent方法的注释4处,调用scrollByInternal方法
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//内部自身滑动
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
//剩余的滑动距离
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//将剩余的滑动距离再次分发给处理嵌套滑动的父View
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;
//...
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
方法内部首先调用scrollStep方法自身滑动,然后计算出剩余的滑动距离。然后将剩余的滑动距离再次分发给处理嵌套滑动的父View。
@Override
public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) {
getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
最终会调用NestedScrollingParent2的onNestedScroll方法。
到目前为止,NestedScrolling2和NestedScrolling并没有差别。
回到RecyclerView的onTouchEvent方法的注释5处,调用fling方法。从这里开始NestedScrolling2和NestedScrolling的处理逻辑产生了差异。
public boolean fling(int velocityX, int velocityY) {
//...
//注释1处
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
//注释2处
dispatchNestedFling(velocityX, velocityY, canScroll);
//...
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//注释3处
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
//注释4处
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
注释1处,先询问外层控件是否要处理惯性滑动,如果外层控件处理了,fling方法直接返回false,自身不滑动。
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
方法内部会调用外层控件的onNestedPreFling方法。
如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling方法返回了true,内层控件自身不滑动。
如果!dispatchNestedPreFling(velocityX, velocityY)
为true,说明外层控件没有处理惯性滑动。注释2处:
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
方法内部会调用外层控件的onNestedFling方法。
注释3处,这里和NestedScrolling有差异。
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
注释3处,外层控件没有处理惯性滑动,那么内层控件就要开始自身的惯性滑动,在开始惯性滑动之前,会调用startNestedScroll方法,通知外层控件内层控件,这时候的滑动类型是TYPE_NON_TOUCH
。
NestedScrollingChildHelper的startNestedScroll方法
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
//根据嵌套滑动的类型来获取NestedScrollingParent
if (hasNestedScrollingParent(type)) {
// 嵌套滑动已经在处理过程中,直接返回true
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//遍历父级控件
while (p != null) {
//注释1处
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
//注释2处
setNestedScrollingParentForType(type, p);
//注释3处
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
注释1处,如果外层控件要处理TYPE_NON_TOUCH
类型的滚动,外层控件的onStartNestedScroll
返回true。
注释3处,外层控件调用onNestedScrollAccepted方法。
我们回到fling方法的注释4处
//注释4处
mViewFlinger.fling(velocityX, velocityY);
ViewFlinger的fling方法。
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
//fling
mOverScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
//请求重新绘制
postOnAnimation();
}
Scroller怎么实现滚动的,这里就不展开了。
ViewFlinger实现了Runnable接口。
@Override
public void run() {
//...
final OverScroller scroller = mOverScroller;
//如果滚动还没结束
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int unconsumedX = x - mLastFlingX;
int unconsumedY = y - mLastFlingY;
mLastFlingX = x;
mLastFlingY = y;
int consumedX = 0;
int consumedY = 0;
// Nested Pre Scroll
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//注释1处,每一帧的计算出来的滚动距离先分发到外层控件
if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
TYPE_NON_TOUCH)) {
//注释2处,减去外层控件消耗的距离
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
}
// Local Scroll
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//注释3处,自身滚动
scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
//减去自身滚动的距离
unconsumedX -= consumedX;
unconsumedY -= consumedY;
}
// Nested Post Scroll
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//注释4处,将剩余的滑动距离分发给外层控件
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
TYPE_NON_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
//...
//继续请求重绘
postOnAnimation();
}
}
注释1处,每一帧的计算出来的滚动距离先分发到外层控件,外层控件调用onNestedPreScroll先滚动。
注释2处,减去外层控件消耗的距离。
注释3处,自身滚动。
注释4处,将剩余的滑动距离分发给外层控件。外层控件调用onNestedScroll来决定是否要进行处理。
这里再总结一下
-
NestedScrolling2分发滚动事件的时候区分了滚动事件的类型:是正常的触摸滚动还是惯性滑动。
-
内层控件先调用
dispatchNestedPreFling
来处理惯性滑动。如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling
方法返回了true。那么NestedScrolling2和NestedScrolling的惯性滑动效果没有什么差异。 -
如果外层控件没有处理惯性滑动,也就是外层控件的
onNestedPreFling
方法返回了false。那么就会调用dispatchNestedFling
方法并且内层控件自身开始惯性滑动mViewFlinger.fling(velocityX, velocityY)
,但是在惯性滑动的每一帧,通过Scroller计算出来的滚动距离通过dispatchNestedPreScroll
先分发给外层控件。外层控件可以通过onNestedPreScroll
先消耗部分滚动距离,然后内层控件再自身滚动。
网友评论