嵌套滑动温习小结

嵌套滑动机制相比传统事件分发在方向、体验和复杂性上有所改进,允许子控件到父控件的方向处理,提供更流畅的体验。开启嵌套滑动的关键在于子View在ACTION_DOWN事件中调用相应方法,并处理剩余滑动距离。实现嵌套滑动不一定需要协调布局和Behavior,自定义View实现NestedScrollingChild或NestedScrollingParent接口即可。在不同版本中,通过不同方式实现接口和方法来支持嵌套滑动。

NestedScrollingParent:以下简称“NSP”,NestedScrollingChild:以下简称“NSC”,这两个接口是嵌套滑动的精髓,是我们实现嵌套滑动遵守的“规则”。

NOTE:本博文非详细原理篇,原理分析网上资料很多!!

与传统事件分发相比,好在哪?

  • 方向维度

    -  传统事件分发机制

      方向:父控件 -> 子控件,且是一锤子买卖,即事件总是先传递给父控件,父控件不拦截该事件,则该事件才会传递给子控件,一旦父控件拦截了该事件,则子View就没有机会处理该事件了,同一事件序列的事件均由父控件处理。

同一事件序列:从手指按下屏幕的那刻起,到抬起手指,这中间产生的一系列事件:一个ACTION_DOWN、N个ACTION_MOVE、一个ACTION_UP等。

    -  嵌套滑动机制

      方向:子控件 -> 父控件,且不是一锤子买卖,且嵌套滑动由子控件来触发,且在同一事件序列中,父控件和子控件都有机会处理该事件。

  • 体验维度

   注意:传统的事件分发机制一样可以实现嵌套滑动。

    -  传统事件分发机制

       体验相对较差,基于传统事件分发与拦截的特点,实现的嵌套滑动不连贯,比如向上滑动到一定位置之后,如吸顶的位置,如果用户不抬起手指,则页面就不会继续向上滑动了。为啥?因为不抬起手指,产生的事件还是同一事件序列,还是由父View消耗,子View没机会消耗。

    -  嵌套滑动机制

        由于同一事件序列,父子View都有机会参与处理,因此体验较好。至于怎么做到[同一事件序列,父子View都有机会参与]的,本文后面会讲。

  • 复杂维度

       嵌套滑动机制实现嵌套滑动更容易一些,为啥?

       传统事件分发机制需要父控件首先拦截处理滑动事件,当到达比如吸顶位置了,此时滑动事件不再满足父控件的处理要求了,此时子View才有机会处理后面的滑动(此处用户应该抬一把手指,o(∩_∩)o )。

        而嵌套滑动机制则不需要,因为官方(>=5.0)SDK中的View和ViewGroup已经以普通方法的方式实现了NSC和NSP中的所有方法了。那是不是意味着不需要我们开发者参与,View和ViewGroup就智能的展示出嵌套滑动的效果了?

        非也。上面说了,View和ViewGroup是以普通方法的方式实现了NSC和NSP中的方法的,而不是回调方法,所以是否开启嵌套滑动,以及如何去使用这些已经实现的方法去使用嵌套滑动,那是开发者自己的事情。

  • 相同点

为了实现连贯的嵌套滑动,谷歌在Lollipop(Android 5.0)时,推出了NestedScrolling机制。该机制并没有脱离传统的事件分发机制,而是在原有的事件分发机制之上,为系统的自带的ViewGroup和View都增加了手势滑动与处理fling的方法。(版本兼容问题,后面说)

如何开启嵌套滑动?

是否开启嵌套滑动是View自己的事情,触发嵌套滑动的节点是子View的ACTION_DOWN事件处理 - 事件拦截和事件处理回调中对此事件的处理,当然也要求它有一个支持嵌套滑动的父View,不然还玩个毛线。

嵌套滑动必须用协调布局和Behavior吗?

非必须,自定义View实现NSP或者NSC即可实现嵌套滑动。协调布局也是一个实现了NSP的View,Behavior只是协调布局特有的用于实现子View联动的手段。通过协调布局和Behavior只是实现嵌套滑动的一种便捷方式。

实现NSC的子View是否必须是支持嵌套滑动的父View的直接子View呢?

非必须,协调布局对Behavior有这个要求,即直接子View设置Behavior才有效,否则,无效。

版本兼容问题

上面说了,嵌套滑动是5.0出现的,那么5.0之前呢?

  • 5.0之前,提供了NSP和NSC接口,以及对应的帮助类!!!开发者实现NSP和NSC即可!!
  • 5.0之后,View或者ViewGroup中已经以普通方法的形式实现了NSC和NSP中的所有方法!!

流程 - 详析

注意:实现嵌套滑动,关键点:

  • 子View:主要的工作是调用时机,及调用完成后,对剩余距离的滑动处理。

        -  5.0 之前,子View实现NSC,并在对应NSC的实现方法中直接调用对应的NSCHelper里面的方法就可以了。然后就是关注调用时机,及调用完成后对剩余距离的处理,比如滑动等。

public class NestedScrollingChildView extends View implements NestedScrollingChild {

    private NestedScrollingChildHelper nestedScrollingChildHelper = new NestedScrollingChildHelper(this);

    public NestedScrollingChildView(Context context) {
        super(context);
    }

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

    // 以下是实现NSC所需要的所有方法
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        nestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return nestedScrollingChildHelper.isNestedScrollingEnabled();
    }

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

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

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

    @Override
    public boolean hasNestedScrollingParent() {
        return nestedScrollingChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

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

        -  5.0 之后,只需关注调用时机,及调用完成后对剩余距离的处理,比如滑动等。

  • 父View:

       在对应的NSP方法中实现距离处理逻辑即可。(不借助NSPHelper也没啥问题,NSPHelper中的逻辑很少)

分析主要从子View着手,因为父View的方法都是子View主动调用的

分析NestedScrollingChildHelper的源码,因为NSCHelper中方法对应方法的逻辑就是嵌套滑动的对子View的规则实现

-      开始 -  startNestedScroll - 调用时机:ACTION_DOWN事件

    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    
    if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        // 子View要支持嵌套滑动,需要调用setNestedScrollingEnabled设置为true
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
            // 如果父View接收嵌套滑动
            // 子View会调用父View的onStartNestedScroll和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;
                }
            // 找到一个支持嵌套滑动的父View
                p = p.getParent();
            }
        }
        return false;
    }

-      滑动前,先通知支持嵌套滑动的父View,父View会根据业务选择滑动或者不滑动

        -   dispatchNestedPreScroll  -  调用时机  -  ACTION_MOVE

        -   可以看到 父View的onNestedPreScroll方法被调用,之后子View可以通过consumed数组得到消耗了多少距离

    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            ......
            // 父View的onNestedPreScroll方法
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

           ......
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

该方法返回true,父View消耗了一段距离,false - 未消耗。

调用完此方法,子View就可以继续完成剩余距离的滑动了。

-  滑动完成,仍然要通知父View

  -  调用dispatchNestedScroll方法,在此方法中,父View的onNestedScroll方法被调用;

其余方法都是类似,略了。一句话,子View行动前,需先通知父View,父View处理完了,子View收拾剩下的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值