文章目录
安卓嵌套滑动机制
此篇文章基于传统的事件分发,读者需要先理解传统事件分发的流程,请看这篇文章
嵌套滑动机制是为了解决传统事件分发不能联动的问题,假设说现在有个需求,要实现的效果如下:
一个顶部Bar
和RecyclerView
,在滑动时产生联动效果。下滑时若Bar
没有显示则先显示Bar
,上滑时若Bar
显示则先隐藏Bar
。
试想使用传统的处理机制可以实现吗,答案是可以。
若两个View
需要协调那必然需要通信,两个View
的联系又是是什么呢?View
是树形结构,两个View
的联系就是上层父View
。RecyclerView
和Bar
需要协调就需要通过上层的Layout
,当RecyclerView
在onTouchEvent
中处理事件时,我们可以先问问上层View
需不需要协调事件,让上层View
去协调处理。
处理是需要回调的,回调是需要设计接口的,谷歌充分考虑了开发者的难处,帮助我们设计了两种接口。
分别是父View
需要实现的NestedScrollingParent和子View
需要实现的NestedScrollingChild
NestedScrollingChild拿到事件就可以先询问NestedScrollingParent,NestedScrollingParent可以根据业务的需要选择自己消费还是给其他子View
消费,事件方向如下:
NestedScrollingChild —> NestedScrollingParent —> View
RecyclerView
已经实现了NestedScrollingChild3,NestedScrollingChild3是NestedScrollingChild的升级版,先分析原始版,再分析升级版。
下面查看两个接口声明的方法
NestedScrollingChild
子View
需要实现的接口
接口解析
public interface NestedScrollingChild {
//设置是否支持嵌套滑动,true则支持,false则不支持
void setNestedScrollingEnabled(boolean enabled);
//获取是否支持嵌套滑动
boolean isNestedScrollingEnabled();
//嵌套滑动事件开始,axes参数是滑动方向,有三种值
//public static final int SCROLL_AXIS_NONE = 0; 没有方向
//public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0; 水平
//public static final int SCROLL_AXIS_VERTICAL = 1 << 1; 垂直
boolean startNestedScroll(@ScrollAxis int axes);
//嵌套滑动事件结束
void stopNestedScroll();
//是否存在响应嵌套滑动事件的父View
boolean hasNestedScrollingParent();
//子View获取到Move事件则回调此方法,先让父View处理
//@param dx,dy是滑动的距离,
//@param consumed是父View消费的距离,在父View中修改此数组,当前View则可知道父消费的距离
//@param offsetInWindow是滑动之前和滑动之后的偏移量
//@return 返回值是父是否消费
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
//将事件再次分发给父View
//再次的意思是dispatchNestedPreScroll已经分发过一次给父View,自己也处理了,但是还有剩余的距离,此时需要再问问父View还要不要
//@param dxConsumed,dyConsumed 是消费了的距离
//@param dxUnconsumed,dyUnconsumed 是未消费的距离
//@param offsetInWindow 是滑动之前和滑动之后的偏移量
//@return 返回值是否存在嵌套滑动
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
//在dispatchNestedPreFling之后回调,这时父和自身已经处理过了,这时父是第二次获取惯性滑动
//@param velocityX,velocityY为惯性滑动距离
//@param consumed表示View是否消费过事件
//@return 返回值为是否存在嵌套滑动
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
//子在处理惯性滑动之前,先回调父View的此方法,让父先处理惯性滑动,父处理完自己再选择是否处理
//@param velocityX,velocityY惯性滑动的距离
//@return 返回值为是否消费距离
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
上述注释只是标准的写法,其某个方法的作用也会根据业务的不同而有所不同。
再看此接口的两个升级版NestedScrollingChild2
,NestedScrollingChild3
NestedScrollingChild2
NestedScrollingChild2如下
public interface NestedScrollingChild2 extends NestedScrollingChild {
//只是多出一个type参数,查看type的定义,看下面
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的值
//手势的输入类型来自用户触摸屏幕
public static final int TYPE_TOUCH = 0;
//手势的输入类型不是由用户触摸屏幕引起的,这通常来自正在安定的一夜情。
public static final int TYPE_NON_TOUCH = 1;
NestedScrollingChild3
NestedScrollingChild3
如下
public interface NestedScrollingChild3 extends NestedScrollingChild2 {
//在NestedScrollingChild2的基础上多出来consumed参数,代表父View消费的距离,在父View中修改此数组,当前View则可知道父消费的距离
void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
@Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
@NonNull int[] consumed);
}
RecyclerView
实现了NestedScrollingChild
,查看RecyclerView
中的实现:
RecyclerView中NestedScrollingChild的实现
RecyclerView
实现了NestedScrollingChild2
和NestedScrollingChild3
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
...
}
重点分析RecyclerView
中的onTouchEvent
,此方法是处理滚动事件的入口,因为嵌套滚动设计的思想就是子View
在处理传统事件时询问父View
是否响应,而子View
处理传统事件的方法就是onTouchEvent()
方式,若读者不理解,可以先看笔者之前写的事件分发机制。
代码分析
RecyclerView#onTouchEvent
重点不是onTouchEvent
的本身逻辑,而是NestedScrollingChild
中方法的调用时机
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
case MotionEvent.ACTION_DOWN: {
...
//嵌套滑动的开始,参数传入滑动方法和触发方式
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) {
//初始化消费距离,此时都没有消费为0
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//回调dispatchNestedPreScroll,让父View先处理事件
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
//父已经处理一部分,此时滑动的距离要减去一部分
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// 更新偏移量
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
//...
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//父提前处理完毕,自身开始处理滑动事件,看下1
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
...
}
} break;
...
case MotionEvent.ACTION_UP: {
//使用速度跟踪器处理计算fill的距离
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;、
//调用fill方法处理惯性滚动,看下2
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
//看下3
resetScroll();
} break;
case MotionEvent.ACTION_CANCEL: {
//看下4
cancelScroll();
} break;
}
/...
return true;
}
1.scrollByInternal
recyclerView
处理自身滑动的方法
RecyclerView#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;
}
...
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;
...
//只有当RecyclerView允许过度滚动时才会触发,消费剩余的距离
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
...
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
2.fling
处理RecyclerView
的惯性滚动
RecyclerView#fling
public boolean fling(int velocityX, int velocityY) {
...
//询问此次滚动事件,父是否响应,只用返回false时RecyclerView自己才可以处理滚动
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
//再次询问父类是否处理事件
dispatchNestedFling(velocityX, velocityY, canScroll);
//处理自身滚动
if (canScroll) {
...
//再次调用startNestedScroll,只是之前传入的模式是TOUCH,此次为NON_TOUCH
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
//计算滚动距离
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
//自身滚动
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
3.resetScroll
RecyclerView#resetScroll
private void resetScroll() {
...
//取消嵌套滑动
stopNestedScroll(TYPE_TOUCH);
...
}
4.cancelScroll
RecyclerView#cancelScroll
private void cancelScroll() {
//间接取消嵌套滚动
resetScroll();
...
}
总结
总结如下:
在down事件中调用startNestedScroll()
方法,开启嵌套滚动事件
在move事件中调用dispatchNestedPreScroll()
让父View先处理滚动,父View
处理完毕再调用scrollByInternal()
方法处理自身滚动,自身滚动完毕再调用dispatchNestedScroll()
方法让父View处理自身处理完的还剩余的距离,若父执行完dispatchNestedScroll()
还有剩余的距离,且当前View
支持过度滚动的话则继续处理剩余的距离。
在up事件中调用fling处理惯性滚动事件,在fling()
方法中调用dispatchNestedPreFling()
将惯性滚动交给父View
,此时dispatchNestedPreFling()
在RecyclerView
的作用是是否拦截此次滑动,若拦截则全权交由父View
,若不拦截则在调用dispatchNestedFling()
方法询问父View
是否处理滑动,父View
处理完则自身再进行处理,惯性滚动并不会记录父消耗多少,这也是和普通事件的区别,自身处理会先调用startNestedScroll()
重新设置触发方式,再调用 mViewFlinger.fling()
进行滚动,整个fling()
执行完毕后会执行resetScroll()
方法,其中又会调用stopNestedScroll()
方法停止嵌套滑动。
在cancel事件中也会调用resetScroll()
再调用stopNestedScroll()
方法停止嵌套滚动。
虽然调用NestedScrollingChild
中的方法就可以回调父类,那么具体是怎么回调的呢
NestedScrollingChildHelper
NestedScrollingChild 会通过 NestedScrollingChildHelper 调用到父View
比如RecyclerView
在调用任何NestedScrollingChild
中的方法时都通过了NestedScrollingChildHelper
,NestedScrollingChildHelper
是NestedScrollingChild
中方法的代理。
查看RecyclerView
中的startNestedScroll()
,看看如何使用的NestedScrollingChildHelper
。
RecyclerView#startNestedScroll
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
RecyclerView#getScrollingChildHelper
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
NestedScrollingChildHelper#startNestedScroll
public boolean startNestedScroll(@ScrollAxis int axes) {
return startNestedScroll(axes, TYPE_TOUCH);
}
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
//如果之前存在可响应的Parent则直接返回true
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
//支持嵌套滑动则命中if
if (isNestedScrollingEnabled()) {
//向上回溯
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//调用父View的onStartNestedScroll(),通知父View要开始嵌套滑动了,看下1
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
//保存此次嵌套滚动的视图的模式,看下2
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
//保存节点,并往上回溯
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
1.ViewParentCompat.onStartNestedScroll
ViewParentCompat
,以Compat
结尾的类大多为适配类,ViewParentCompat
也不例外,会判断父View
具体实现的NestedScrollingParent
去执行方法,NestedScrollingChild
是子实现的,而NestedScrollingParent
就是父去实现的,目前不需要知道此接口的方法,我们只分析他是怎么调到上层View
的。
//代码很简单
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
if (Build.VERSION.SDK_INT >= 21) {
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) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
上述方法中有两个参数child
和target
,child
永远是此次回溯最近的View
,而target
是获取事件的View
,只是为了解决多层嵌套的问题。
举例:假设父View
只有一个RecyclerView
,若调用此方法则child = target = RecyclerView
若爷View
嵌套一个父View
再嵌套一个RecyclerView
,第一次调用父View
的onStartNestedScroll()
方法child = target = RecyclerView,若父View
的onStartNestedScroll()
方法返回为false
,触发回溯,第二次调用爷View
的onStartNestedScroll()
方法,此时child = 父View,target = RecyclerView
2.ViewParentCompat.onNestedScrollAccepted
和onStartNestedScroll()
一样也是做适配
public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedScrollAccepted(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onNestedScrollAccepted", e);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
nestedScrollAxes);
}
}
}
总结:NestedScrollingChildHelper
中封装了自动寻找父类的方法,通过View
的getParent()
方法获取父View
,并通过ViewParentCompat
适配类对父View
实现的接口版本进行判断再去具体执行回调。
NestedScrollingParent
上述我们已经了解到怎么去回调的父View
,那么回调父类的哪些方法呢,接下来分析NestedScrollingParent,NestedScrollingParent还有两个升级版,分别是NestedScrollingParent2,NestedScrollingParent3,目前只分析原版。
接口分析
public interface NestedScrollingParent {
//嵌套事件开始,子View调用startNestedScroll会回调此方法
//@Param child当前View的直接子View
//@Param target真实响应事件的View
//@Param axes滑动方向
//@Return 是否响应此次嵌套滑动事件
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
//用于保存此次嵌套滚动的方向信息、
//@Param child当前View的直接子View
//@Param target真实响应事件的View
//@Param axes滑动方向
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
//结束此次滚动事件,多用于重置触发方式,触发方式就是上述说的NestedScrollType
//@Param target真实响应事件的View
void onStopNestedScroll(@NonNull View target);
//子View处理完调用此方法,询问父类还需不需要消费事件
//@Param target真实响应事件的View
//@Param dxConsumed 消费了的横向距离
//@Param dyConsumed 消费了的纵向距离
//@Param dxUnconsumed 未消费的横向距离
//@Param dyUnconsumed 未消费的纵向距离
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
//子View处理之前先调用此方法询问父View需不需要滑动
//@Param target真实响应事件的View
//@Param dx产生的横向滑动距离
//@Param dy产生的纵向滑动距离
//@Param consumed父滑动的距离
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
//子View处理完惯性滚动后调用此方法,询问父类还需不需要消费惯性滚动事件
//@Param target真实响应事件的View
//@Param velocityX 惯性滑动产生的横向距离
//@Param velocityY 惯性滑动产生的纵向距离
//@Param consumed 父消费的距离
//@Return 父是否消费
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
//子View处理惯性滚动之前先调用此方法询问父View需不需要滑动
//@Param target真实响应事件的View
//@Param velocityX 惯性滑动产生的横向距离
//@Param velocityY 惯性滑动产生的纵向距离
//@Return 父是否消费
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
//获取滑动方向
@ScrollAxis
int getNestedScrollAxes();
}
接下来看一下NestedScrollingParent2
和NestedScrollingParent3
NestedScrollingParent2
对应NestedScrollingChild2
NestedScrollingParent3
对应NestedScrollingChild3
NestedScrollingParent2
public interface NestedScrollingParent2 extends NestedScrollingParent {
//只是多出一个type参数,在NestedScrollingChild2已分析
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type);
}
NestedScrollingParent3
多出来consumed
参数,之前在NestedScrollingChild3
中已经分析
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);
}
那上述接口怎么使用呢,看以下NestedScrollView
中的实现
NestedScrollView中NestedScrollingParent的实现
NestedScrollView
不仅实现了NestedScrollingParent3还实现了NestedScrollingChild3
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView {
...
}
先分析接口中方法的调用时机
onStartNestedScroll和onNestedScrollAccepted已经分析过,当子View
调用startNestedScroll()
则会调用NestedScrollingChildHelper#startNestedScroll,再调用ViewParentCompat
中的onStartNestedScroll()
和onNestedScrollAccepted()
方法进行回调。
其他方法也是这样,当子View
调用,则会通过NestedScrollingChildHelper
和ViewParentCompat
进行回调。
具体的对应如下:
NestedScrollingChild | NestedScrollingParent |
---|---|
startNestedScroll() | onStartNestedScroll() onNestedScrollAccepted() |
stopNestedScroll() | onStopNestedScroll() |
dispatchNestedScroll() | onNestedScroll() |
dispatchNestedPreScroll() | onNestedPreScroll() |
dispatchNestedFling() | onNestedFling() |
dispatchNestedPreFling() | onNestedPreFling() |
在RecyclerView中NestedScrollingChild的实现小节中讲述了NestedScrollingChild中方法的调用时机和作用,因此对应过来看在NestedScrollingParent是怎么处理的
拿onNestedScrollAccepted
方法举例
NestedScrollView#onNestedScrollAccepted
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
int type) {
//借助NestedScrollingParentHelper进行代理
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
//NestedScrollView也可能存在父View处理嵌套滑动,因此向上回溯
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}
NestedScrollingParentHelper
NestedScrollingChild
使用NestedScrollingChildHelper进行方法代理,在NestedScrollingParent
则使用NestedScrollingParentHelper进行代理,他们两个的不同是NestedScrollingChild
所有的方法基本都需要代理,而NestedScrollingParent
只是某些方法需要代理,一般是onNestedScrollAccepted()
方法和onStopNestedScroll()
方法。因为这两个方法无论谁实现,处理都是一样的,先分析这两个方法。
NestedScrollingParentHelper#onNestedScrollAccepted
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
//保存此次触发的方式,是触摸还是惯性滚动
if (type == ViewCompat.TYPE_NON_TOUCH) {
mNestedScrollAxesNonTouch = axes;
} else {
mNestedScrollAxesTouch = axes;
}
}
NestedScrollingParentHelper#onStopNestedScroll
public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
//clear本次滑动的触发模式
if (type == ViewCompat.TYPE_NON_TOUCH) {
mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;
} else {
mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;
}
}
NestedScrollView对子View滚动事件的具体处理
NestedScrollView
的三个方法并没有对事件做过多的处理,只是将事件往上分发了,事件交给了NestedScrollView
的上层View
这三个方法分别如下:
NestedScrollView#onNestedPreScroll
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
//dispatchNestedPreScroll是NestedScrollingChild中的方法,主要是把事件交给父View,这和后面两个方法一样,都是直接交给父View去处理
dispatchNestedPreScroll(dx, dy, consumed, null, type);
}
NestedScrollView#onNestedPreFling
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
NestedScrollView#onNestedFling
@Override
public boolean onNestedFling(
@NonNull View target, float velocityX, float velocityY, boolean consumed) {
//没有消费,则交给父View去处理
if (!consumed) {
dispatchNestedFling(0, velocityY, true);
fling((int) velocityY);
return true;
}
return false;
}
只有onNestedScroll进行了自己的处理,处理也很简单,只是消费掉滚动滑动中没有消费的距离
NestedScrollView#onNestedScroll
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
onNestedScrollInternal(dyUnconsumed, type, null);
}
NestedScrollView#onNestedScrollInternal
private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
final int oldScrollY = getScrollY();
//消费掉没有消费的举例
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
if (consumed != null) {
consumed[1] += myConsumed;
}
final int myUnconsumed = dyUnconsumed - myConsumed;
//再次往上分发
mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}
总结
NestedScrollView
并没有真正的处理嵌套滑动而是交给上层View
去处理,我们在使用NestedScrollView
一般会结合CoordinatorLayout
来用,CoordinatorLayout
中存在真正的处理逻辑。(本篇文章不对CoordinatorLayout
进行分析)
若我们想实现文章开头的效果,则需要自己实现NestedScrollingParent
接口,下面我们进行实现。
实现自己的NestedScrollingParent
创建NestedScrollingParentLayout继承LinearLayout
并实现NestedScrollingParent
先不谈NestedScrollingParentLayout的具体实现,先说如何使用
我们要实现的效果是NestedScrollingParentLayout的第一个View
是Bar
,是可以跟随滑动的
<com.hbsd.mdviewdemo.NestedScrollingParentLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="MissingDefaultResource">
<TextView
android:background="@color/teal_700"
android:text="可滑动Bar"
android:gravity="center_vertical"
android:textSize="20sp"
android:textColor="#fff"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="50dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.hbsd.mdviewdemo.NestedScrollingParentLayout>
具体实现如下:
//因为在这个需求中,没有必要真正让Bar滑动,父View自身滑动即可,因此滑动事件都由自己处理。
class NestedScrollingParentLayout : LinearLayout, NestedScrollingParent {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int)
: super(context, attrs, defStyleAttr)
init {
//设置默认方向为垂直
orientation = VERTICAL
}
//可滑动Bar的高度
private var mTopViewHeight = 0
//属性动画
lateinit private var mValueAnimator: ValueAnimator
//初始化方法代理类
private val mNestedScrollingParentHelper = NestedScrollingParentHelper(this)
//仿照NestedScrollView实现,必须是垂直才可以响应嵌套滑动
override fun onStartNestedScroll(
child: View,
target: View,
@ViewCompat.ScrollAxis axes: Int,
): Boolean {
return axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
}
//保存此次滑动的触发模式
override fun onNestedScrollAccepted(child: View, target: View, @ViewCompat.ScrollAxis axes: Int) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes)
}
//清楚本次滑动的触发模式
override fun onStopNestedScroll(target: View) {
mNestedScrollingParentHelper.onStopNestedScroll(target)
}
//不需要实现,因为onNestedScroll是处理父处理过且子也处理过一次剩余的距离,这里没必要实现
override fun onNestedScroll(
target: View, dxConsumed: Int, dyConsumed: Int,
dxUnconsumed: Int, dyUnconsumed: Int,
) {
}
//父提前处理滚动事件
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
//判断当前是否应该隐藏Bar,若滑动的方向是上,且没有超过mTopViewHeight则代表可以隐藏
val hideTop = dy > 0 && getScrollY() < mTopViewHeight
//判断当前是否应该隐藏Bar,若滑动的方向是下,且没有到达顶端则需要显示Bar
val showTop = dy < 0 && getScrollY() >= 0 && (!target.canScrollVertically(-1) || !isTop())
if (hideTop || showTop) {
if (hideTop) {
Log.e("onNestedPreScroll", "hideTop")
} else {
Log.e("onNestedPreScroll", "showTop")
}
//移动距离
scrollBy(0, dy)
//记录消费的距离
consumed[1] = dy
}
}
//Bar是否在top
private fun isTop() = if (scrollY == 0) true else false
//重写scrollTo,处理越界问题,之前吃过这方面的教训,读者可以自己试一下,如果不重写,可能导致隐藏不全,或者显示不全,原因就是滑动的距离没有和0,mTopViewHeight做较大值和较小值比较
override fun scrollTo(x: Int, y: Int) {
val y = if (y < 0) {
0
} else if (y > mTopViewHeight) {
mTopViewHeight
} else {
y
}
super.scrollTo(x, y)
}
//当视图大小发生改变时,初始化mTopViewHeight
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (getChildAt(0) != null) {
mTopViewHeight = getChildAt(0).measuredHeight
}
}
//处理父处理过一次且子处理过一次剩余的惯性滚动滑动距离,我们的逻辑是只要触发滚惯性动则完全隐藏或显示Bar
override fun onNestedFling(
target: View,
velocityX: Float,
velocityY: Float,
consumed: Boolean,
): Boolean {
//滑动动画时间0.2秒
val duration: Int = 200
if (velocityY > 0) { //向上滑
startAnimation(duration.toLong(), scrollY, mTopViewHeight)
} else if (velocityY < 0) { //向下滑动
startAnimation(duration.toLong(), scrollY, 0)
}
return true
}
//属性动画,开始滑动
private fun startAnimation(duration: Long, startY: Int, endY: Int) {
if (!::mValueAnimator.isInitialized) {
mValueAnimator = ValueAnimator()
mValueAnimator.addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Int
scrollTo(0, animatedValue)
}
} else {
mValueAnimator.cancel()
}
mValueAnimator.interpolator = DecelerateInterpolator()
mValueAnimator.setIntValues(startY, endY)
mValueAnimator.duration = duration
mValueAnimator.start()
}
override fun getNestedScrollAxes(): Int {
return mNestedScrollingParentHelper.nestedScrollAxes
}
//如果返回true则直接拦截,不会再调用后续的onNestedFling,这是RecyclerView的逻辑决定的,这必须返回为false
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float) = false
}
笔者还是要说转Kotlin
的必要性,19年Kotlin就成为安卓指定的官方开发语言了,Java
的版本虽然一直在升级,但对于安卓来说永远是1.8,目前许多语言都有强大的语法糖,而Kotlin是集大成者,借助这些语法糖能极大简化代码
对于安卓来讲,Java
是旧时代的残党,新时代里没有载他的船,虽然Google
明确说不会抛弃Java
,但是转Kotlin
笔者认为还是很有必要的
笔者之前记录了学习Kotlin的过程,文章在这
最终效果即为开篇的效果
Demo私信我哦
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!