在Android的事件分发机制里面,当一个View决定消耗事件流时,其它的View就不能再处理这个事件流的了,所以对于有嵌套滑动的地方就要用到NestedScrollingParent和NestedScrollingChild。最新的是NestedScrollingParent2和NestedScrollingChild2是在NestedScrollingParent和NestedScrollingChild的基础上扩展的,相对于旧版本他们对嵌套监听提供了触摸类型的区分,使得fling也可以进行嵌套滚动。要想实现嵌套滑动这两个接口必须成对出现。
组成
已知实现了NestedScrollingParent的ViewGroup有NestedScrollView、CoordinatorLayout、SwipeRefreshLayout
等。已知实现了NestedScrollingChild的接口有BaseGridView、HorizontalGridView、NestedScrollView、RecyclerView、SwipeRefreshLayout、VerticalGridView
。所以NestedScrollView、SwipeRefreshLayout
既实现类Parent接口也实现类Child接口。
先了解一下Parent和Child接口的组成
public interface NestedScrollingParent2 extends NestedScrollingParent {
/**
* 这个是嵌套滑动控制事件分发的控制方法,只有返回true才能接收到事件分发
* @param child 包含target的ViewParent的直接子View
* @param target 发起滑动事件的View
* @param axes 滑动的方向,数值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
* {@link ViewCompat#SCROLL_AXIS_VERTICAL}
* @return true 表示父View接受嵌套滑动监听,否则不接受
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type);
/**
* 这个方法在onStartNestedScroll返回true之后在正式滑动之前回调
* @param child 包含target的父View的直接子View
* @param target 发起嵌套滑动的View
* @param axes 滑动的方向,数值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
* {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type);
/**
*
* @param target View that initiated the nested scroll
*/
void onStopNestedScroll(@NonNull View target);
/**
* 在子View滑动过程中会分发这个嵌套滑动的方法,要想这里收到嵌套滑动事件必须在onStartNestedScroll返回true
* @param dxConsumed 子View在水平方向已经消耗的距离
* @param dyConsumed 子View在垂直方法已经消耗的距离
* @param dxUnconsumed 子View在水平方向剩下的未消耗的距离
* @param dyUnconsumed 子View在垂直方法剩下的未消耗的距离
* @param type 发起嵌套事件的类型 分为触摸(ViewParent.TYPE_TOUCH)和非触摸(ViewParent.TYPE_NON_TOUCH)
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
/**
* 在子View开始滑动之前让父View有机会先进行滑动处理
* @param dx 水平方向将要滑动的距离
* @param dy 竖直方向将要滑动的距离
* @param consumed Output. 父View在水平和垂直方向要消费的距离,consumed[0]表示水平方向的消耗,consumed[1]表示垂直方向的消耗,
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type);
}
以上就是NestedScrollingParent2主要的方法介绍,下面看看NestedScrollingChild2的方法
public interface NestedScrollingChild2 extends NestedScrollingChild {
//返回值true表示找到了嵌套交互的ViewParent,type表示引起滑动事件的类型,这个事件和parent中的onStartNestedScroll是对应的
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
//停止嵌套滑动的回调
void stopNestedScroll(@NestedScrollType int type);
//表示有实现了NestedScrollingParent2接口的父类
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);
}
嵌套滑动的原理
下面给出的是NestedScrollView中的onTouchEvent方法的源码,整个嵌套滑动事件和View的事件分发是结合在一起的,相对于在原来view的事件分发里面加了滑动回调给父类,并且把滑动的距离算出来。这样一看就很清晰了。
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
...
//嵌套滑动的原理是滑动事件先从子View开始,在子View接收到ACTION_DOWN事件的时候开始寻找是否有嵌套滑动的父类并且回调onStartNestedScroll方法
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE:
...
//在ACTION_MOVE开始时先分发嵌套滑动之前的事件,最后回调onNestedPreScroll方法
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (mIsBeingDragged) {
...
//滑动过程中回调onNestedScroll方法
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
}
break;
case MotionEvent.ACTION_UP:
...
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
//松开手回调fling类型的onNestedScroll方法
flingWithNestedDispatch(-initialVelocity);
}
//回调stopNestedScroll方法
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
...
//回调stopNestedScroll方法
endDrag();
break;
}
...
vtev.recycle();
return true;
}
NestedScrollingChildHelper和NestedScrollingParentHelper
这两个是实际处理嵌套逻辑的代理类,谷歌把嵌套滑动的逻辑已经封装在里面,在需要的地方实现类逻辑的复用,这样设计的好处是避免和onTouchEvent里面事件处理逻辑的耦合,让逻辑更加清晰,调用方便,并且helper里面的方法和接口的方法是一一对应方法名相同的。这种设计模式值得我们在代码里面学习和使用。我们可以看看子View是怎么样找到有嵌套监听等父类的,我们以onTouchEvent里面startNestedScroll方法,最终来到了NestedScrollingChildHelper里面的startNestedScroll方法
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
//已经在嵌套滑动过程中
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
//首先检查是否可以嵌套滑动,因为像recyclerview中是可以关闭嵌套滑动的
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//遍历寻找实现了NestedScrollingParent接口的父类
while (p != null) {
//调用父类的onStartNestedScroll方法,如果返回ture则告诉父类的onNestedScrollAccepted方法已经已经收到了父类的请求
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和NestedScrollingParent进行区分
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;
}
实例
这里截取了知乎首页内容滚动和tab联动的例子,当然这个实现的方法有很多,这里用嵌套滑动的方法去实现。逻辑是内容部分是一个实现了NestedScrollingChild2的RecyclerView,当然这部分不用我们去做,RecyclerView本来就实现了,父类是一个实现了NestedScrollingParent2接口的ViewGroup,这里可以用FrameLayout。当RecyclerView滚动时,通知FrameLayout的实现下方Tab栏的滚动逻辑,就这么简单。
首先自定义ViewGroup
public class NestedFrameLayout extends FrameLayout implements NestedScrollingParent2 {
public NestedFrameLayout(Context context) {
super(context);
}
public NestedFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NestedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int i, int i1) {
return true;
}
@Override
public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) {
}
@Override
public void onStopNestedScroll(@NonNull View view, int i) {
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (mListener != null) {
if (Math.abs(dyConsumed)>5){
if (dyConsumed>0){
mListener.onScroll(true);
}else {
mListener.onScroll(false);
}
}
}
}
@Override
public void onNestedPreScroll(@NonNull View view, int i, int i1, @NonNull int[] ints, int i2) {
}
public Listener mListener;
public void setListener(Listener listener) {
mListener = listener;
}
public interface Listener {
void onScroll(boolean isScrollUp);
}
}
然后在MainActivity监听这个滑动事件和处理tab的联动,并且加上动画就看到了下图的效果了
@Override
public void onScroll(boolean isScrollUp) {
if (isScrollUp) {
hideBottomNavigationBar();
} else {
showBottomNavigationBar();
}
}
private void showBottomNavigationBar() {
if (!mIsNavigationBarHide) {
animateOffset(mBottomTabLayout.getHeight());
mIsNavigationBarHide = true;
}
}
private void hideBottomNavigationBar() {
animateOffset(0);
mIsNavigationBarHide = false;
}
private void animateOffset(final int offset) {
if (mTranslationAnimator == null) {
mTranslationAnimator = ViewCompat.animate(mBottomTabLayout);
mTranslationAnimator.setDuration(300);
mTranslationAnimator.setInterpolator(new LinearOutSlowInInterpolator());
mTranslationAnimator.setUpdateListener(new ViewPropertyAnimatorUpdateListener() {
@Override
public void onAnimationUpdate(View view) {
}
});
} else {
mTranslationAnimator.cancel();
}
mTranslationAnimator.translationY(offset).start();
}
这里面就可以做到上方无论切到哪个tab滑动时,下方的tab都能联动,因为从上面NestedScrollingChildHelper的分析可知RecyclerView会遍历寻找想要监听嵌套滑动的ViewGroup。这样就做到了全局联动。