Android嵌套滑动讲解

作者 : 开心源码 本文共7865个字,预计阅读时间需要20分钟 发布时间: 2022-05-12 共175人阅读

在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栏的滚动逻辑,就这么简单。

image

首先自己设置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。这样就做到了全局联动。

image

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android嵌套滑动讲解

发表回复