美文网首页
NestScollling 机制原理分析和使用

NestScollling 机制原理分析和使用

作者: 天馬_1bdd | 来源:发表于2017-02-28 15:19 被阅读0次

    目录

    0.引言

    1.利用分发机制实现嵌套滑动的不足

    2.NestedScrolling设计的独特之处

    3.NestedScrolling 原理分析

    3.1相关接口和类
    3.2NestedScrollingChild接口
    3.3NestedScrollingParent接口

    4.流程梳理

    5分发机制和Nested机制代码实现对比

    5.1场景分析
    5.2布局代码
    5.3分发机制实现
    5.4Nested机制实现
    5.5小结

    0.引言

    在android系统版本5.0(API 21)之前,是没有官方控件支持嵌套滑动的。要实现类似ScrollView嵌套ListView或ScrollView嵌套ScrollView这些功能,往往要在自定义控件上作成百上千行的复杂处理,且耦合度高、性能底下、代码实现难度大。

    Android 在发布 Lollipop(5.0)版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性。

    1.利用分发机制实现嵌套滑动的不足

    在Lollipop(5.0)版本之前,想要实现嵌套滑动,必须在分发机制中处理。我们知道Android对Touch事件的分发是有自己一套机制的。主要是有是三个函数:
    dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent

    网上流传

    这种分发机制有一个漏洞:
    如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。
    也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。

    //事件分发伪代码逻辑,源自《Android开发艺术探索》章节:3.4.1
    public boolean dispatchTouchEvent(MotionEvent event) {
            boolean consume = false;
            if (onInterceptTouchEvent(event)) {
                consume = onTouchEvent(event);
            } else {
                consume = child.dispatchTouchEvent(event);
            }
            return consume;
        }```
    
    ![Android事件分发流程图.png](https://img.haomeiwen.com/i4969082/d964da0734867008.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    #2 NestedScrolling设计的独特之处
    为了解决不能够一次按下拖动到底的痛点,NestedScrolling机制能够让父view和子view在滚动时进行配合,其基本流程如下:
    >当子view开始滚动之前,可以通知父view,让其先于自己进行滚动;
    >子view自己进行滚动
    >子view滚动之后,还可以通知父view继续滚动
    
    ![coordinator_sample.gif](https://img.haomeiwen.com/i4969082/876d97104b6a392c.gif?imageMogr2/auto-orient/strip)
    
    
    要实现这样的交互,父View需要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口。
    
    在这套交互机制中,child是动作的发起者,parent只是接受回调并作出响应。
    
    另外:父view和子view并不需要是直接的父子关系,即如果“parent1包含parent2,parent2包含child”,则parent1和child仍能通过nestedScrolling机制进行交互。
    

    public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
    // Already in progress
    return true;
    }
    if (isNestedScrollingEnabled()) {
    ViewParent p = mView.getParent();
    View child = mView;
    while (p != null) {
    if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
    mNestedScrollingParent = p;
    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
    return true;
    }
    if (p instanceof View) {
    child = (View) p;
    }
    p = p.getParent();
    }
    }
    return false;
    }

    static class ViewParentCompatStubImpl implements ViewParentCompatImpl {

        @Override
        public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }
    
    
    

    public class NestedScrollView implements NestedScrollingParent{
    ...
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
    ...
    }

    #3.NestedScrolling 原理分析
    
    ##3.1相关接口和类
    
    ![图片2.png](https://img.haomeiwen.com/i4969082/333a0414def6cd98.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    >主要接口:NestedScrollingChild、NestedScrollingParent
    >帮助类:NestedScrollingChildHelper、NestedScrollingParentHelper
    
    使用NestedScrolling机制,父View需要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口。
    
    而NestedScrollingChildHelper和NestedScrollingParentHelper是两个帮助类,当我们在实现NestedScrollingChild和NestedScrollingParent接口时,使用这两个帮助类可以简化我们的工作。
    
    以上接口和类都在support-v4包中提供。另外,一些较新的系统view都已经实现了NestedScrollingChild或NestedScrollingParent接口,也就是说他们直接支持NestedScrolling,例如:
    >NestedScrollView 已实现 NestedScrollingParent和NestedScrollingChild
    >RecyclerView 已实现 NestedScrollingChild
    >CoordinatorLayout 已实现 NestedScrollingParent 
    >...
    
    ##3.2NestedScrollingChild接口
    
    ###3.2.1接口概述
    

    public interface NestedScrollingChild {
    //开始、停止嵌套滚动
    public boolean startNestedScroll(int axes);
    public void stopNestedScroll();

    //触摸滚动相关
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
    
    //惯性滚动相关
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
    

    }

    
    ####3.2.1.1
    `public boolean startNestedScroll(int axes);`
    - 开启嵌套滚动流程(实际上是进行了一些嵌套滚动前准备工作)。
    - 当找到了能够配合当前子view进行嵌套滚动的父view时,返回值为true(Returns:true if a cooperative parent was found and nested scrolling has been enabled for the current gesture)。
    
    
    ####3.2.1.2
    `public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);`
    - 在子view自己进行滚动之前调用此方法,询问父view是否要在子view之前进行滚动。
    - 此方法的前两个参数用于告诉父View此次要滚动的距离;而第三第四个参数用于子view获取父view消费掉的距离和父view位置的偏移量。
    - 第一第二个参数为输入参数,即常规的函数参数,调用函数的时候我们需要为其传递确切的值。而第三第四个参数为输出参数,调用函数时我们只需要传递容器(在这里就是两个数组),在调用结束后,我们就可以从容器中获取函数输出的值。
    - 如果parent消费了一部分或全部距离,则此方法返回true。
    
    
    ####3.2.1.3
    `public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);`
    
    - 在子view自己进行滚动之后调用此方法,询问父view是否还要进行余下(unconsumed)的滚动。
    - 前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。
    - 返回值:(翻译出来可能有歧义,直接放原文)true if the event was dispatched, false if it could not be dispatched.
    
    
    ####3.2.1.4
    最后,stopNestedScroll()方法与startNestedScroll(int axes)对应,用于结束嵌套滚动流程;而惯性滚动相关的两个方法与触摸滚动相关的两个方法类似,这里不再赘述。
    
    ###3.2.2接口实现
    上面只是讲了接口中的主要方法和调用时机,那么这些方法具体该如何实现呢?这时候就要用到上面提到的帮助类了。具体操作很简单:首先实例化一个帮助类对象,然后在要实现的接口方法中调用帮助类对象中的同名方法即可——帮助类对象已经帮我们完成了一切。
    

    public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
    NestedScrollingChild, ScrollingView
    {
    //........................
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
    mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }
    
    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }
    
    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }
    
    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }
    
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }
    
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
    
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }
    
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
    

    //........................
    }

    ##3.3NestedScrollingParent接口
    ###3.3.1 接口概述
    

    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 onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
    
    //当惯性嵌套滚动时被调用
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    

    }

    从命名可以看出,这几个都是回调方法。当调用NestedScrollingChild中的方法时,NestedScrollingParent中与之相对应的方法就会被回调。方法之间的具体对应关系如下:
    
    | 子(发起者)       | 父(被同步调用)          |
    | ------------- |:-------------:| 
    | startNestedScroll     | onStartNestedScroll、onNestedScrollAccepted |
    | dispatchNestedPreScroll      | onNestedPreScroll|  
    | dispatchNestedScroll | onNestedScroll      |   
    | dispatchNestedPreFling| onNestedPreFling|   
    | dispatchNestedFling| onNestedFling|   
    | stopNestedScroll| onStopNestedScroll     |   
    
    ####3.3.1.1
    `public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);`
    
    | 参数      | 说明         |
    | ------------- |:-------------| 
    | target:| 发起嵌套滚动的子View,此子view必须实现NestedScrollingChild接口。上面提到过,此子view并不需要是当前view的直接子view |
    | child:| 当前view的包含target的直接子view|  
    | nestedScrollAxes:| 嵌套滚动的方向,可能是SCROLL_AXIS_HORIZONTAL 或 SCROLL_AXIS_VERTICAL 或 二者都有    |   
    
    ####3.3.1.2 
    `onNestedPreScroll()、onNestedPreScroll()、onNestedPreFling()、onNestedFling()`
    这几个方法分别对应NestedScrollingChild中的dispatchNestedPreScroll()、dispatchNestedScroll()、dispatchNestedPreFling()和dispatchNestedFling()。
    
    它们的参数也是基本对应的,以onNestedPreScroll()为例,参数dx、dy、consumed实际就是dispatchNestedPreScroll()中的dx、int dy、consumed。
    ###3.3.2
    onNestedScrollAccepted、onStopNestedScroll的实现同样是调用帮助类中的同名方法即可:
    
    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    ...
    }
    
    @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
    

    ...
    }

    #4 流程梳理
    经过以上的介绍,我们可以大致将嵌套滚动的流程概括如下(以触摸滚动为例,惯性滚动(fling)的流程与此类似):
    >1.调用child的startNestedScroll()来发起嵌套滚动流程(实质是寻找能够配合child进行嵌套滚动的parent)。parent的onStartNestedScroll()会被回调,如果此方法返回true,则onNestedScrollAccepted()也会被回调。
    
    >2.child每次滚动前,可以先询问parent是否要滚动,即调用dispatchNestedPreScroll(),这会回调到parent的onNestedPreScroll(),parent可以在这个回调中先于child滚动。
    
    >3.dispatchNestedPreScroll()之后,child可以进行自己的滚动操作。
    child滚动以后,可以调用dispatchNestedScroll(),会回调到parent的onNestedScroll(),在这里parent可以进行后于child的滚动。
    
    >4.滚动结束,调用stopNestedScroll()。
    
    调用时序图:
    
    ![调用时序图.png](https://img.haomeiwen.com/i4969082/b0160bcea8f480b8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    
    #5分发机制和Nested机制代码实现对比
    先看效果:
    
    ![嵌套实现效果.gif](https://img.haomeiwen.com/i4969082/e2d80a727661d43a.gif?imageMogr2/auto-orient/strip)
    
    ##5.1场景分析
    当头部可见时:父控件滑动
    当头部不可见时:子控件滑动
    
    头部从可见到不可见时:控制权从父控件→子控件
    头部从可见到不可见时:控制权从子控件→父控件
    
    ##5.2布局代码:
    ![布局层次图.png](https://img.haomeiwen.com/i4969082/1a0cabd951dd09d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    StickyNavLayout是公用父类,负责处理子View的初始化、页面测量和滚动实现:
    

    public class StickyNavLayout extends LinearLayout
    {
    protected static final String TAG = "StickyNavLayout";

    protected View mTop;
    protected View mNav;
    protected ViewPager mViewPager;
    protected int mTopViewHeight;
    protected OverScroller mScroller;
    
    public StickyNavLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        setOrientation(LinearLayout.VERTICAL);
        mScroller = new OverScroller(context);
    }
    
    @Override
    protected void onFinishInflate()
    {
        super.onFinishInflate();
        mTop = findViewById(R.id.id_stickynavlayout_topview);
        mNav = findViewById(R.id.id_stickynavlayout_indicator);
        View view = findViewById(R.id.id_stickynavlayout_viewpager);
        if (!(view instanceof ViewPager))
        {
            throw new RuntimeException(
                    "id_stickynavlayout_viewpager show used by ViewPager !");
        }
        mViewPager = (ViewPager) view;
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        //不限制顶部的高度
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        getChildAt(0).measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
        params.height = getMeasuredHeight() - mNav.getMeasuredHeight();
        setMeasuredDimension(getMeasuredWidth(), mTop.getMeasuredHeight() + mNav.getMeasuredHeight() + mViewPager.getMeasuredHeight());
    
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);
        mTopViewHeight = mTop.getMeasuredHeight();
    }
    
    @Override
    public void scrollTo(int x, int y)
    {
        if (y < 0)
        {
            y = 0;
        }
        if (y > mTopViewHeight)
        {
            y = mTopViewHeight;
        }
        if (y != getScrollY())
        {
            super.scrollTo(x, y);
        }
    }
    
    @Override
    public void computeScroll()
    {
        if (mScroller.computeScrollOffset())
        {
            scrollTo(0, mScroller.getCurrY());
            invalidate();
        }
    }
    
    public void fling(int velocityY) {
        mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
        invalidate();
    }
    

    }

    
    ##5.3分发机制实现
    

    public class DispatchStickyNavLayout extends StickyNavLayout {

    protected static final String TAG = "DispatchStickyNavLayout";
    
    private ViewGroup mInnerScrollView;
    private boolean isTopHidden = false;
    
    private VelocityTracker mVelocityTracker;
    private int mTouchSlop;
    private int mMaximumVelocity, mMinimumVelocity;
    
    private float mLastY;
    private boolean mDragging;
    
    private boolean isInControl = false;
    
    public DispatchStickyNavLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context)
                .getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context)
                .getScaledMinimumFlingVelocity();
    
    }
    
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        float y = ev.getY();
    
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG,"dispatchTouchEvent:ACTION_DOWN");
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG,"dispatchTouchEvent:ACTION_MOVE");
                float dy = y - mLastY;
                getCurrentScrollView();
    
                if (mInnerScrollView instanceof ScrollView||mInnerScrollView instanceof NestedScrollView) {
                  //嵌套子类滑动到顶&头部视图隐藏&向上滑动
                    if (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0
                            && !isInControl) {
                        isInControl = true;
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        dispatchTouchEvent(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        return dispatchTouchEvent(ev2);
                    }
                } else if (mInnerScrollView instanceof ListView) {
    
                    ListView lv = (ListView) mInnerScrollView;
                    View c = lv.getChildAt(lv.getFirstVisiblePosition());
    
                    if (!isInControl && c != null && c.getTop() == 0 && isTopHidden
                            && dy > 0) {
                        isInControl = true;
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        dispatchTouchEvent(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        return dispatchTouchEvent(ev2);
                    }
                } else if (mInnerScrollView instanceof RecyclerView) {
    
                    RecyclerView rv = (RecyclerView) mInnerScrollView;
    
                    if (!isInControl && android.support.v4.view.ViewCompat.canScrollVertically(rv, -1) && isTopHidden
                            && dy > 0) {
                        isInControl = true;
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        dispatchTouchEvent(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        return dispatchTouchEvent(ev2);
                    }
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
    
    /**
     *
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        float y = ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG,"onInterceptTouchEvent:ACTION_UP");
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG,"onInterceptTouchEvent:ACTION_MOVE");
                float dy = y - mLastY;
                getCurrentScrollView();
                if (Math.abs(dy) > mTouchSlop) {
                    mDragging = true;
                    if (mInnerScrollView instanceof ScrollView||mInnerScrollView instanceof NestedScrollView) {
                        // 如果topView没有隐藏
                        // 或嵌套子视图滑动到顶 && topView隐藏 && 上滑,则拦截
                        if (!isTopHidden
                                || (mInnerScrollView.getScrollY() == 0
                                && isTopHidden && dy > 0)) {
    
                            initVelocityTrackerIfNotExists();
                            mVelocityTracker.addMovement(ev);
                            mLastY = y;
                            return true;
                        }
                    } else if (mInnerScrollView instanceof ListView) {
    
                        ListView lv = (ListView) mInnerScrollView;
                        View c = lv.getChildAt(lv.getFirstVisiblePosition());
                        // 如果topView没有隐藏
                        // 或sc的listView在顶部 && topView隐藏 && 上滑,则拦截
    
                        if (!isTopHidden || //
                                (c != null //
                                        && c.getTop() == 0//
                                        && isTopHidden && dy > 0)) {
    
                            initVelocityTrackerIfNotExists();
                            mVelocityTracker.addMovement(ev);
                            mLastY = y;
                            return true;
                        }
                    } else if (mInnerScrollView instanceof RecyclerView) {
                        RecyclerView rv = (RecyclerView) mInnerScrollView;
                        if (!isTopHidden || (!android.support.v4.view.ViewCompat.canScrollVertically(rv, -1) && isTopHidden && dy > 0)) {
                            initVelocityTrackerIfNotExists();
                            mVelocityTracker.addMovement(ev);
                            mLastY = y;
                            return true;
                        }
                    }
    
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.e(TAG,"onInterceptTouchEvent:ACTION_CANCEL");
            case MotionEvent.ACTION_UP:
                Log.e(TAG,"onInterceptTouchEvent:ACTION_UP");
                mDragging = false;
                recycleVelocityTracker();
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(event);
        int action = event.getAction();
        float y = event.getY();
    
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG,"onTouchEvent:ACTION_DOWN");
                if (!mScroller.isFinished())
                    mScroller.abortAnimation();
                mLastY = y;
                return true;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG,"onTouchEvent:ACTION_MOVE");
                float dy = y - mLastY;
    
                Log.e("TAG", "dy = " + dy + " , y = " + y + " , mLastY = " + mLastY);
    
                if (!mDragging && Math.abs(dy) > mTouchSlop) {
                    mDragging = true;
                }
                if (mDragging) {
                    scrollBy(0, (int) -dy);
    
                    // 如果topView隐藏,且向下滑动时,则改变当前事件为ACTION_DOWN
                    if (getScrollY() == mTopViewHeight && dy < 0) {
                        event.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(event);
                        isInControl = false;
                    }
                }
    
                mLastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.e(TAG,"onTouchEvent:ACTION_CANCEL");
                mDragging = false;
                recycleVelocityTracker();
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG,"onTouchEvent:ACTION_UP");
                mDragging = false;
                //计算一秒内的移动的像素,且不会超过
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocityY = (int) mVelocityTracker.getYVelocity();
                if (Math.abs(velocityY) > mMinimumVelocity) {
                    fling(-velocityY);
                }
                recycleVelocityTracker();
                break;
        }
    
        return super.onTouchEvent(event);
    }
    
    private void getCurrentScrollView() {
        int currentItem = mViewPager.getCurrentItem();
        PagerAdapter a = mViewPager.getAdapter();
        if (a instanceof FragmentPagerAdapter) {
            FragmentPagerAdapter fadapter = (FragmentPagerAdapter) a;
            Fragment item = (Fragment) fadapter.instantiateItem(mViewPager,
                    currentItem);
            mInnerScrollView = (ViewGroup) (item.getView()
                    .findViewById(R.id.id_stickynavlayout_innerscrollview));
        } else if (a instanceof FragmentStatePagerAdapter) {
            FragmentStatePagerAdapter fsAdapter = (FragmentStatePagerAdapter) a;
            Fragment item = (Fragment) fsAdapter.instantiateItem(mViewPager,
                    currentItem);
            mInnerScrollView = (ViewGroup) (item.getView()
                    .findViewById(R.id.id_stickynavlayout_innerscrollview));
        }
    
    }
    
    @Override
    public void scrollTo(int x, int y) {
        super.scrollTo(x, y);
        isTopHidden = getScrollY() == mTopViewHeight;
    }
    
    
    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }
    
    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }
    

    }

    ###5.3.1 onTouchEvent
    被拦截后的事件会在这个函数处理
    1.在ACTION_DOWN的时候记录下y的值
    2.在ACTION_MOVE时,计算出与上次的偏差,调用scrollBy滑动,如果滑动到topView隐藏了,那么分发一个ACTION_DOWN事件,然后事件最终会分发到内部ScrollView处理(关键1:此处是把事件分派给子)
    3.ACTION_CANCEL时,回收VelocityTracker(速度追踪器),停止动画
    4.ACTION_UP时,计算速度,判断是否有惯性滑动,然后调用fling方法
    

    public void fling(int velocityY) {
    mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
    invalidate();
    }

    ###5.3.2  onInterceptTouchEvent
    onInterceptTouchEvent默认是不拦截的,父类方法返回false
    1.在ACTION_DOWN的时候记录下y的值
    2.ACTION_MOVE时,计算出与上次的偏差,情况1:如果topView没隐藏,则拦截事件,return true;情况2:如果topView隐藏且向上滑动且子控件滑动滑动到顶,拦截事件
    
    ###5.3.3 dispatchTouchEvent
    如果事件能够传递给当前View,那么此方法一定会被调用
    1.在ACTION_DOWN的时候记录下y的值
    2.ACTION_MOVE时,向上滑动到顶部即将可见时,先发起一个ACTION_CANCEL表示结束当前分发响应流程。再分发一个ACTION_DOWN事件,重走流程。此事件最终会到onTouch事件中(关键2:此处是把事件重新交回父类)
    
    ###5.3.4 分发机制小结
    整个流程能够一个手势按下后无缝连接运转,关键在于以下几点
    1.在dispatchTouchEvent的ACTION_MOVE函数时,在符合条件下再次分发一个ACTION_CANCEL和ACTION_DOWN事件
    2.在onInterceptTouchEvent的ACTION_MOVE函数时,如果topView没隐藏,则主动拦截下事件在父类中处理,否则交给子类
    3.onTouchEvent的ACTION_MOVE函数时,滑动到topView隐藏后,分发一个ACTION_DOWN事件,把事件交给子类处理
    
    
    ##5.4Nested机制实现
    

    public class NestedStickyNavLayout extends StickyNavLayout implements NestedScrollingParent {

    private NestedScrollingParentHelper mParentHelper;
    
    private static final String TAG = "NestedStickyNavLayout";
    
    public NestedStickyNavLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.e(TAG, "onStartNestedScroll");
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
    
    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        getScrollingParentHelper().onNestedScrollAccepted(child, target, nestedScrollAxes);
        Log.e(TAG, "onNestedScrollAccepted");
    }
    
    @Override
    public void onStopNestedScroll(View target) {
        getScrollingParentHelper().onStopNestedScroll(target);
        Log.e(TAG, "onStopNestedScroll");
    }
    
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        Log.e(TAG, "onNestedScroll");
    }
    
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        Log.e(TAG, "onNestedPreScroll");
        boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
        boolean showTop = dy < 0 && getScrollY() >= 0 && !ViewCompat.canScrollVertically(target, -1);
    
        if (hiddenTop || showTop) {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }
    
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        Log.e(TAG, "onNestedFling");
        return false;
    }
    
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        Log.e(TAG, "onNestedPreFling");
        //down - //up+
        if (getScrollY() >= mTopViewHeight) return false;
        fling((int) velocityY);
        return true;
    }
    
    @Override
    public int getNestedScrollAxes() {
        Log.e(TAG, "getNestedScrollAxes");
        return getScrollingParentHelper().getNestedScrollAxes();
    }
    
    private NestedScrollingParentHelper getScrollingParentHelper() {
        if (mParentHelper == null) {
            mParentHelper = new NestedScrollingParentHelper(this);
        }
        return mParentHelper;
    }
    

    }

    用Nested机制实现,只需实现NestedScrollingParent 接口的八个方法即可。虽然看起来方法多,但实现起来代码量却少很多。
    在`onNestedScrollAccepted(),onStopNestedScroll(),getNestedScrollAxes()`函数内,只需调用NestedScrollingParentHelper 的同名同参函数即可。
    
    `onStartNestedScroll()`函数的一般写法就是
    `return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;`
    ,表示其接受垂直方向的滚动。
    
    剩下的就是四个方法
    `onNestedPreScroll(),onNestedScroll(),onNestedPreFling(),onNestedFling()`
    
    onNestedPreScroll:头部视图尚未完全消失,视图向上滑动隐藏头部或向下滑动引出头部时,父类完全消耗滑动距离,并把消耗量记录在consumed中以供回调
    
    onNestedScroll:因为onNestedPreScroll已完全消耗,所以这里不再作处理
    
    onNestedPreFling:如果scrollY大于头部高度,返回false,表示不消耗。否则,则相应惯性滑动,调用fling()方法
    
    onNestedFling:不二次响应惯性滑动
    
    ##5.5小结
    分发机制要实现内容:
    >1.分发函数dispatchTouchEvent
    2.拦截函数onInterceptTouchEvent
    3.触摸事件函数onTouchEvent
    4.速度追踪器VelocityTracker:
    5.找到并直接持有嵌套子View(强耦合)
    
    Nested机制要实现内容:
    >实现NestedScrollingParent 接口(主要onNestedPreScroll(),onNestedScroll(),onNestedPreFling(),onNestedFling()方法)
    
    [comment]:***
     #6总结
    兼容性比较:
    因为分发机制可以持有嵌套子View,所以可以兼容ListView、ScrollView、NestedScrollView、RecyclerView等。
    Nested机制只能支持NestedScrollView、RecyclerView这类实现了NestedScrollingChild接口的滑动控件
    
    性能:
    减少了层级依赖,提高视图绘制效率,减轻了CPU计算能耗
    
    开发效率:
    比传统分发机制实现简单,条理会更加清晰。只需在相应函数中写入业务要求的处理代码即可
    
    在此机制上,google还发布了一个强大的控件CoordinatorLayout,一个FrameLayout。该布局的强大在于,能够协调子元素之间的依赖关系。

    相关文章

      网友评论

          本文标题:NestScollling 机制原理分析和使用

          本文链接:https://www.haomeiwen.com/subject/uomxgttx.html