NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动

你也许没见过 NestedScrollingParent 和NestedScrollingChild这两个接口,但你或多或少听过嵌套滑动。就像下图一样,顶部随着下滑出现,上滑隐藏。如果使用传统的事件分发来写的话,不仅复杂还容易出错。
这里写图片描述
而使用NestedScrollingParent 和NestedScrollingChild来实现的话就简单多了,虽然本质也是基于事件分发,但是谷歌爸爸已经帮我们都封装好了。这里写图片描述


那么 NestedScrollingParent 和NestedScrollingChild是怎么实现滑动的联动的呢。说到底,这两个只是个接口,还等待我们去实现。别担心,我们只需要定义两个view实现这两个接口,而两个接口的关联类已经有了。现在先来了解一下这两个接口。

既然有Parent和Child,我就可以理解为分别和父视图和子视图相关。具体来说,就是通过捕捉实现了NestedScrollingChild的view的事件,来通知实现了NestedScrollingParent 的view去做点什么事

我们先来看NestedScrollingChild

public interface NestedScrollingChild {
    /**
     * 设置是否允许嵌套滑动,允许的话设为true
     */
    public void setNestedScrollingEnabled(boolean enabled);

    /*
     * 是否允许嵌套滑动
     */
    public boolean isNestedScrollingEnabled();

    /**
     * 开始嵌套滑动
     */
    public boolean startNestedScroll(int axes);

    /**
     * 结束嵌套滑动
     */
    public void stopNestedScroll();

    /**
     * 判断NestedScrollingParent 的onStartNestedScroll方法是否返回true,只有true,才能继续一系列的嵌套滑动
     */
    public boolean hasNestedScrollingParent();

    /**
     * 子view消费了拖动事件之后通知父view,
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    /**
     *子view消费了拖动事件之前通知父view,dx dy是将要消费的距离,如果父view要消费可通过
     *设置consumed[0]=x .consumed[1]=y来分别消费x,y。然后子view继续处理剩下的位移(即dx-x,dy-y)
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /**
     *子view消费了滑动事件之后通知父view,
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     *子view消费了滑动事件之前通知父view
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

再看看NestedScrollingParent

public interface NestedScrollingParent {
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    public void onStopNestedScroll(View target);
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
    public int getNestedScrollAxes();
}

有没有发现两个接口很相似,只是把dispatch开头的换成了on开头。
一般来说on开头的方法代表的都是回调方法,我以startNestedScroll方法为例。当子view开始滑动时,会调用startNestedScroll方法,在该方法里面,主要实现就是获取当前的view的父view,如果父view实现了NestedScrollingParent 接口,就会回调onStartNestedScroll。然后在这些回调实现一些界面的变动,就有联动的效果了。


前面说了,两个视图的关联,谷歌已经帮我们实现好了。就是NestedScrollingChildHelper和NestedScrollingParentHelper。

NestedScrollingParentHelper的实现比较简单,就只是保存滑动的方向,事实上不使用它也没什么关系

public class NestedScrollingParentHelper {
    private final ViewGroup mViewGroup;
    private int mNestedScrollAxes;

    /**
     * Construct a new helper for a given ViewGroup
     */
    public NestedScrollingParentHelper(ViewGroup viewGroup) {
        mViewGroup = viewGroup;
    }

    public void onNestedScrollAccepted(View child, View target, int axes) {
        mNestedScrollAxes = axes;
    }

    public int getNestedScrollAxes() {
        return mNestedScrollAxes;
    }

    public void onStopNestedScroll(View target) {
        mNestedScrollAxes = 0;
    }
}

而NestedScrollingChildHelper的作用就是负责关联两个view了
当子view开始滑动的使用,会调用NestedScrollingChildHelper的startNestedScroll方法,来启动嵌套滑动。
从下面的代码可以知道,NestedScrollingChild 的startNestedScroll会回调NestedScrollingParent的onStartNestedScroll
后续的几个方法基本也是这种调用模式

public boolean startNestedScroll(int axes) {
        //验证一个滑动流程只可以startNestedScroll一次
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        //启动滑动,子view要调用setNestedScrollingEnabled方法启动。
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                //判断父view 是否实现NestedScrollingParent 且接口的onStartNestedScroll方法返回true
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    //调用NestedScrollingParent 的onNestedScrollAccepted回调方法
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

从两个接口和NestedScrollingChildHelper和NestedScrollingParentHelper,我发现,虽然NestedScrollingChildHelper和NestedScrollingParentHelper没有实现两个接口,但是定义的方法名字是一样的。这种代理模式不但便于理解,也更好的解耦


通过这四个东西,就可以实现一些联动效果了,NestedScrollingChild 的实现类有很多,比如recyclerview和nestscrollview。一般我们都是基于这些视图滚动进行关联。
下面我定义一个粉色条,根据scrollview的上下滚动而进行上下滑动。效果如下
这里写图片描述

实现起来其实很简单,因为NestedScrollView实现了NestedScrollingChild接口,NestedScrollView里面又根据滑动情况已经处理好了NestedScrollingChildHelper的调用,在这里,我只要专注NestedScrollingParent 的实现就可以了

<?xml version="1.0" encoding="utf-8"?>
<com.example.administrator.transdemo.ParentView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.example.administrator.transdemo.ScrollingActivity">

    <android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context="com.example.administrator.transdemo.ScrollingActivity"
        tools:showIn="@layout/activity_scrolling">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/text_margin"
            android:text="@string/large_text" />

    </android.support.v4.widget.NestedScrollView>



    <ImageView
        android:layout_width="10dp"
        android:layout_height="30dp"
        android:layout_gravity="right"
        android:background="@color/colorAccent"/>

</com.example.administrator.transdemo.ParentView>

ParentView 的实现如下

public class ParentView extends FrameLayout implements NestedScrollingParent2 {
    public ParentView(@NonNull Context context) {
        this(context, null);
    }

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

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


    View imageRight;
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        imageRight= getChildAt(1);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        //如果是竖直方向滑动,就启动嵌套滑动
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {

    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
    }

    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        //这里的Consumed代表NestScrollView消耗的距离, Unconsumed代表NestScrollView未消耗的距离
        //imageRight根据NestScrollView滑动的距离而进行相应的滑动、。
        imageRight.setTranslationY(imageRight.getTranslationY() + dyConsumed);

    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {

    }
}

只需要几行代码就实现了两个view的关联滑动。
下面如果我加多一个关联的view,代码如下

public class ParentView extends FrameLayout implements NestedScrollingParent2 {
    public ParentView(@NonNull Context context) {
        this(context, null);
    }

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

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


    View imageLeft;
    View imageRight;
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        imageLeft = getChildAt(1);
        imageRight = getChildAt(2);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {

    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {

    }

    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        imageRight.setTranslationY(imageRight.getTranslationY() + dyConsumed);

    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
        imageLeft.setTranslationY(imageLeft.getTranslationY() + dy);
    }
}

这里写图片描述
imageLeft表示左边的条块,imageRight表示右边的,滑动scrollview的时候,会怎么变化?


答案如下图
这里写图片描述

public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
        //dx dy表示ontouEvent move的时候产生的原始偏移值,NestedScrollView处理它自己的滑动之前先调用这个方法。
        //因为没有调用consumed[1] = xx,消耗掉相应的偏移值,所以他和右边滑块的速度是一样的。
        //而NestedScrollView滑动到顶部的,继续上滑,ontouEvent move照样调用,该方法继续触发,下面的语句继续执行
        //而imageRight 是跟随NestedScrollView偏移值进行相应的偏移,所以imageRight 不会动。
        imageLeft.setTranslationY(imageLeft.getTranslationY() + dy);
    }

上面我使用了NestedScrollingParent2 而不是NestedScrollingParent,其实NestedScrollingParent2 继承了NestedScrollingParent接口,只是在下面方法后面加多了一个参数 @NestedScrollType int type ,该参数有两张情况TYPE_TOUCH, TYPE_NON_TOUCH,表示当前状态是否在手指触摸下

   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, @Nullable int[] consumed,
            @NestedScrollType int type);

NestedScrollingParent 和NestedScrollingChild应用无处不在。使用最多的就是CoordinatorLayout。
CoordinatorLayout + AppBarLayout实现的toolbar滑动就是使用这个原理实现的。当前CoordinatorLayout不止是有嵌套滑动这个效果。但是目前很多酷炫的滑动特效都是基于CoordinatorLayout 实现的

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值