一篇文章教你学会安卓的嵌套滑动

请添加图片描述

安卓嵌套滑动机制

在这里插入图片描述

此篇文章基于传统的事件分发,读者需要先理解传统事件分发的流程,请看这篇文章

嵌套滑动机制是为了解决传统事件分发不能联动的问题,假设说现在有个需求,要实现的效果如下:

一个顶部BarRecyclerView,在滑动时产生联动效果。下滑时若Bar没有显示则先显示Bar,上滑时若Bar显示则先隐藏Bar

试想使用传统的处理机制可以实现吗,答案是可以。

若两个View需要协调那必然需要通信,两个View的联系又是是什么呢?View是树形结构,两个View的联系就是上层父ViewRecyclerViewBar需要协调就需要通过上层的Layout,当RecyclerViewonTouchEvent中处理事件时,我们可以先问问上层View需不需要协调事件,让上层View去协调处理。

处理是需要回调的,回调是需要设计接口的,谷歌充分考虑了开发者的难处,帮助我们设计了两种接口。

分别是父View需要实现的NestedScrollingParent和子View需要实现的NestedScrollingChild

NestedScrollingChild拿到事件就可以先询问NestedScrollingParentNestedScrollingParent可以根据业务的需要选择自己消费还是给其他子View消费,事件方向如下:

NestedScrollingChild —> NestedScrollingParent —> View

RecyclerView已经实现了NestedScrollingChild3NestedScrollingChild3NestedScrollingChild的升级版,先分析原始版,再分析升级版。

下面查看两个接口声明的方法

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);
}

上述注释只是标准的写法,其某个方法的作用也会根据业务的不同而有所不同。

再看此接口的两个升级版NestedScrollingChild2NestedScrollingChild3

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实现了NestedScrollingChild2NestedScrollingChild3

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中的方法时都通过了NestedScrollingChildHelperNestedScrollingChildHelperNestedScrollingChild中方法的代理。

查看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;
}

上述方法中有两个参数childtargetchild永远是此次回溯最近的View,而target是获取事件的View,只是为了解决多层嵌套的问题。

举例:假设父View只有一个RecyclerView,若调用此方法则child = target = RecyclerView

若爷View嵌套一个父View再嵌套一个RecyclerView,第一次调用父ViewonStartNestedScroll()方法child = target = RecyclerView,若父ViewonStartNestedScroll()方法返回为false,触发回溯,第二次调用爷ViewonStartNestedScroll()方法,此时child = 父Viewtarget = 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中封装了自动寻找父类的方法,通过ViewgetParent()方法获取父View,并通过ViewParentCompat适配类对父View实现的接口版本进行判断再去具体执行回调。

NestedScrollingParent

上述我们已经了解到怎么去回调的父View,那么回调父类的哪些方法呢,接下来分析NestedScrollingParentNestedScrollingParent还有两个升级版,分别是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();
}

接下来看一下NestedScrollingParent2NestedScrollingParent3

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 {
	...
}

先分析接口中方法的调用时机

onStartNestedScrollonNestedScrollAccepted已经分析过,当子View调用startNestedScroll()则会调用NestedScrollingChildHelper#startNestedScroll,再调用ViewParentCompat中的onStartNestedScroll()onNestedScrollAccepted()方法进行回调。

其他方法也是这样,当子View调用,则会通过NestedScrollingChildHelperViewParentCompat进行回调。

具体的对应如下:

NestedScrollingChildNestedScrollingParent
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的第一个ViewBar,是可以跟随滑动的

<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}{评论,你的意见是我进步的财富!}

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值