Android NestedScrolling嵌套滑动实现

安卓实现嵌套滚动的机制解析

要知道在安卓中,处理事件分发的机制和滑动冲突都是通过View的dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent这三个方法互相配合完成的。
但是这种机制是由父View发起,一直向下传递,而且这个机制有个缺点:

父View将事件分发下去之后,一旦有子View决定处理该系列事件(即子View的onTouchEvent返回了true),那么该系列事件将会一直下发给子View处理,即使子View在某个onTouch事件中不想处理(即返回false),那它的父View也不会再继续处理此系列事件,除非父View拦截某个事件,但是父View拦截了touch事件之后,该系列事件就不会下发给子View处理了

于是乎就有了NestedScrolling机制了,NestedScrolling机制中,通过子View在处理滑动事件的手告诉父View我要开始滑动了,此时父View便会决定要不要跟着一起动,或者会寻找有没有另外的子View要跟着一起滑动。NestedScrolling机制主要依赖以下四个类:

  1. NestedScrollingChild
  2. NestedScrollingParent
  3. NestedScrollingChildHelper
  4. NestedScrollingParentHelper

一般作为滑动列表的子View实现NestedScrollingChild接口,作为parent的父View则实现NestedScrollingParent,这样滑动的子View就可以将滑动事件分发给父View,并且父View也可以将滑动事件分发给其他的子View了。

贴出NestedScrollingChild和NestedScrollingParent两个接口的API解释:

public interface NestedScrollingChild {

    /**
     * 设置嵌套滑动是否能用
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled);

    /**
     * 判断嵌套滑动是否可用
     */
    @Override
    public boolean isNestedScrollingEnabled();

    /**
     * 开始发起嵌套滑动
     *
     * @param axes 表示方向轴,有横向和竖向
     */
    @Override
    public boolean startNestedScroll(int axes);

    /**
     * 停止嵌套滑动
     */
    @Override
    public void stopNestedScroll();

    /**
     * 判断是否有支持嵌套滑动的父View
     */
    @Override
    public boolean hasNestedScrollingParent() ;

    /**
     * 惯性滑动时调用
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率
     * @param consumed 是否被消费
     * @return  true 如果惯性滑动被父View或其它子View消耗
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) ;

    /**
     * 进行惯性滑动前调用
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率
     * @return true 父View消耗了惯性滑动
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) ;

    /**
     * 子view处理滑动之后调用(即子view滑动完之后通知父view)
     * @param dxConsumed x轴上被消费的距离(横向)
     * @param dyConsumed y轴上被消费的距离(竖向)
     * @param dxUnconsumed x轴上未被消费的距离
     * @param dyUnconsumed y轴上未被消费的距离
     * @param offsetInWindow 子View的窗体偏移量
     * @return  true 事件被父View处理, false 事件没有被父View处理
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) ;

    /**
     * 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离(即在子view处理滑动之前通知父View是否要消耗滑动事件)
     * @param dx  x轴上滑动的距离
     * @param dy  y轴上滑动的距离
     * @param consumed 父view消费掉的滑动长度
     * @param offsetInWindow   子View的窗体偏移量
     * @return 支持的嵌套的父View 是否处理了 滑动事件
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);

}
public interface NestedScrollingParent {
        /**
         * 回应子view发起的嵌套滑动
         * @param child 实现了NestedScrollingParent的view
         * @param target 发起嵌套滑动的子view
         * @param axes 滑动方向
         * @return  true 表示接受嵌套滑动
         */
        boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes);

        /**
         * 接受嵌套滑动时回调
         * @param child 实现了NestedScrollingParent的view
         * @param target 发起嵌套滑动的子view
         * @param axes 滑动方向
         */
        void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes);

        /**
         * 嵌套滑动结束回调
         * @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)
         * @param target 发起嵌套滚动的子view
         * @param dx 消耗的水平滚动距离
         * @param dy 消耗的垂直滚动距离
         * @param consumed 输出此父view消耗的水平和垂直滚动距离
         */
        void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

        /**
         * 开始惯性滑动时回调(后于子view)
         * @param var1
         * @param var2
         * @param var3
         * @param var4
         * @return
         */
        boolean onNestedFling(@NonNull View var1, float var2, float var3, boolean var4);

        /**
         * 惯性滑动之前回调(先于子view)
         * @param var1
         * @param var2
         * @param var3
         * @return
         */
        boolean onNestedPreFling(@NonNull View var1, float var2, float var3);

        /**
         * 返回NestedScrollingParent的滑动方向
         * @return
         */
        int getNestedScrollAxes();
    }

ok,对两个接口的API有了一定的了解之后,现在我们来看一下NestedScrollingChild接口和NestedScrollingParent接口中的方法对应关系:

NestedScrollingChildImpl NestedScrollingParentImpl
startNestedScroll onStartNestedScroll, onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

接下来写个实例验证一下,先上效果图:

首先我们自定义一个YNestedScrollingChild继承自LinearLayout,然后实现NestedScrollingChild接口,并实现里面的方法,一般作为发起嵌套滑动的子View,只需自己实现onTouchEvent,在onTouchEvent里面处理相应的逻辑即可,其他NestedScrollingChild接口里面的方法就交给NestedScrollingChildHelper或它的super View去处理。

说明一点:为什么上面说可以交给super View处理呢?因为在API21或以上,安卓View里卖弄已经帮我们实现了NestedScrolling机制,即不需要我们手动实现NestedScrollingChild和NestedScrollingParent接口了,例如:

public class NestedScrollingChildImpl extends RelativeLayout {
    public NestedScrollingChildImpl(Context context) {
        super(context);
    }

    public NestedScrollingChildImpl(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NestedScrollingChildImpl(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return super.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        super.stopNestedScroll();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @androidx.annotation.Nullable int[] offsetInWindow) {
        return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @androidx.annotation.Nullable int[] consumed, @androidx.annotation.Nullable int[] offsetInWindow) {
        return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
}

当然在低于API21的情况下呢?那就需要我们自己实现NestedScrollingChild和NestedScrollingParent两个接口了,这里以NestedScrollingChildImpl举例:

public class NestedScrollingChildImpl extends RelativeLayout implements NestedScrollingChild {

    private NestedScrollingChildHelper mChildHelper;

    public NestedScrollingChildImpl(Context context) {
        this(context,null);
    }

    public NestedScrollingChildImpl(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public NestedScrollingChildImpl(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mChildHelper = new NestedScrollingChildHelper(this);
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @androidx.annotation.Nullable int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @androidx.annotation.Nullable int[] consumed, @androidx.annotation.Nullable int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
}

到这里相信应该可以解除很大一部分伙计心中的疑虑了。说明:上述两个NestedScrollingChildImpl类的代码仅仅为了说明API21或以上和API21以下的两种实现方式,所以NestedScrollingChildImpl 中只实现了部分接口,而且跟今天要讲的例子没有任何关系,请各位看官忽略。

接下来继续实现上面gif的例子:
先呈现XML代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.dreamer__yy.widget.YNestedScrollingParent
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:scaleType="fitXY"
            android:src="@drawable/icon_header" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="我是要停靠的,我上面的图片是要滑动的"
            android:textColor="@color/white"
            android:textSize="18sp" />

        <com.dreamer__yy.widget.YNestedScrollingChild
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/large_text"
                android:textSize="15sp" />
        </com.dreamer__yy.widget.YNestedScrollingChild>

    </com.dreamer__yy.widget.YNestedScrollingParent>
</RelativeLayout>

接下来直接上实现了NestedScrollingChild的子View的代码:

public class YNestedScrollingChild extends LinearLayout  {

    private NestedScrollingChildHelper childHelper;
    private int preHeight;
    private int maxHeight;
    private float lastY;
    private int[] consumed = new int[2];
    private int[] offset = new int[2];

    public YNestedScrollingChild(Context context) {
        this(context,null);
    }

    public YNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public YNestedScrollingChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        childHelper = new NestedScrollingChildHelper(this);
        childHelper.setNestedScrollingEnabled(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //因为XML里面给YNestedScrollingChild的高设置的时wrap_content,这里得出第一次测量的高度即展示出来的高度
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        preHeight = getMeasuredHeight();
        //将测量模式设置成不限制,得出YNestedScrollingChild的总高度
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        maxHeight = getMeasuredHeight();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //记录第一次触摸的Y坐标
                lastY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                //得到手指触摸的当前的Y坐标
                float rawY = event.getRawY();
                //得到手指滑动的Y的距离
                int dy = (int) (rawY - lastY);
                lastY = rawY;
                //判断时竖直滑动且找到了支持嵌套滑动的父view并且父view处理了滑动
                //consumed是一个空的数组,这里直接传给父View,让父View将自己消耗的Y轴上的距离写进去
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) && dispatchNestedPreScroll(0, dy, consumed, offset)) {
                    //计算父View消耗了滑动事件之后还剩下多少待滑动的距离
                    int scrollY = dy - consumed[1];
                    if (scrollY != 0) {
                        //自己处理剩下的滑动距离
                        scrollBy(0, -scrollY);
                    }
                }else {
                    //不是竖直滑动,或者没找到支持嵌套滑动的父View,或者父view不处理滑动事件,此时自己处理滑动即可
                    scrollBy(0, -dy);
                }
                break;
        }
        return true;
    }

    //设置滑动是否可行,此处交给父View处理即可(API 低于21的就交给NestedScrollingChildHelper处理)
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        super.setNestedScrollingEnabled(enabled);
    }

    //嵌套滑动是否可行,此处注意一定要交给NestedScrollingChildHelper处理
    @Override
    public boolean isNestedScrollingEnabled() {
        return childHelper.isNestedScrollingEnabled();
    }

    //开始嵌套滑动
    @Override
    public boolean startNestedScroll(int axes) {
        return super.startNestedScroll(axes);
    }

    //结束嵌套滑动
    @Override
    public void stopNestedScroll() {
        super.stopNestedScroll();
    }

    //是否有支持嵌套滑动的父View
    @Override
    public boolean hasNestedScrollingParent() {
        return super.hasNestedScrollingParent();
    }

    //子view处理滑动之后调用(即子view滑动完之后通知父view)
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
        return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    //在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离(即在子view处理滑动之前通知父View是否要消耗滑动事件)
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
        return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    //惯性滑动时调用
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return super.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    //进行惯性滑动前调用
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return super.dispatchNestedPreFling(velocityX, velocityY);
    }

    /**
     * 控制view滑动范围,不让view进行多余的滑动
     * @param x
     * @param y
     */
    @Override
    public void scrollTo(int x, int y) {
        y = y < 0 ? 0 : (y > (maxHeight - preHeight) ? (maxHeight - preHeight) : y);
        super.scrollTo(x, y);
    }
}

然后是实现了NestedScrollingParent接口的父View的代码:

public class YNestedScrollingParent extends LinearLayout {

    private NestedScrollingParentHelper parentHelper;
    private int imgHeight = 0;
    private ImageView iv;
    private YNestedScrollingChild scrollingChild;

    public YNestedScrollingParent(Context context) {
        this(context,null);
    }

    public YNestedScrollingParent(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public YNestedScrollingParent(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        parentHelper = new NestedScrollingParentHelper(this);
    }

    /**
     * 布局加载完成回调
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        iv = ((ImageView) getChildAt(0));
        scrollingChild = ((YNestedScrollingChild) getChildAt(2));
        iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (imgHeight <= 0) {
                    imgHeight = iv.getMeasuredHeight();
                }
            }
        });
    }

    /**
     * 控制view滑动范围
     * @param x
     * @param y
     */
    @Override
    public void scrollTo(int x, int y) {
        y = y < 0 ? 0 : (y > imgHeight ? imgHeight : y);
        super.scrollTo(x, y);
    }

    //如果目标view是YNestedScrollingChild,就接受嵌套滑动事件
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return target instanceof YNestedScrollingChild;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        super.onNestedScrollAccepted(child, target, axes);
    }

    @Override
    public void onStopNestedScroll(View child) {
        super.onStopNestedScroll(child);
    }

    //子view滑动后回调(后于子view),此处不做逻辑处理
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    }

    //子view滑动前回调(先于子view)
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        //判断是否需要滑动
        if (showImg(dy) || hideImg(dy)) {
            scrollBy(0,-dy);
            //记录父View消耗的滑动距离
            consumed[1] = dy;
        }

    //获取嵌套滑动方向
    @Override
    public int getNestedScrollAxes() {
        return super.getNestedScrollAxes();
    }

    private boolean showImg(int dy){
        if (dy > 0) {//表示向下滑动
            if (getScrollY() > 0) {
                return true;
            }
        }
        return false;
    }

    private boolean hideImg(int dy){
        if (dy < 0) {//表示向上滑动
            if (getScrollY() < imgHeight) {//控制最大只能滑动imgHeight
                return true;
            }
        }
        return false;
    }
}

有人会说,不是说是实现了NestedScrollingParent接口的父View嘛,有这个疑问的朋友肯定是上课走神了,这个在前面解释过了,当然在API21或以上你也可以自己实现NestedScrollingChild/NestedScrollingParent,然后配合NestedScrollingChildHelper /NestedScrollingParentHelper 完成逻辑处理。

OK,这样就可以实现上面gif图的效果啦,本次内容就到此结束的咯~~

你可能感兴趣的:(Material,Design)