NestScollling 机制原理分析和使用

目录

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](http://upload-images.jianshu.io/upload_images/4969082-d964da0734867008.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

#2 NestedScrolling设计的独特之处
为了解决不能够一次按下拖动到底的痛点,NestedScrolling机制能够让父view和子view在滚动时进行配合,其基本流程如下:
>当子view开始滚动之前,可以通知父view,让其先于自己进行滚动;
>子view自己进行滚动
>子view滚动之后,还可以通知父view继续滚动

![coordinator_sample.gif](http://upload-images.jianshu.io/upload_images/4969082-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](http://upload-images.jianshu.io/upload_images/4969082-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](http://upload-images.jianshu.io/upload_images/4969082-b0160bcea8f480b8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


#5分发机制和Nested机制代码实现对比
先看效果:

![嵌套实现效果.gif](http://upload-images.jianshu.io/upload_images/4969082-e2d80a727661d43a.gif?imageMogr2/auto-orient/strip)

##5.1场景分析
当头部可见时:父控件滑动
当头部不可见时:子控件滑动

头部从可见到不可见时:控制权从父控件→子控件
头部从可见到不可见时:控制权从子控件→父控件

##5.2布局代码:
![布局层次图.png](http://upload-images.jianshu.io/upload_images/4969082-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。该布局的强大在于,能够协调子元素之间的依赖关系。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容