Android NestedScrolling(嵌套滑动)机制

推荐:

http://www.jianshu.com/p/aff5e82f0174
http://blog.csdn.net/lmj623565791/article/details/52204039#reply
搭配使用效果更佳

前言:

在android 5.0之前要是想要实现嵌套滑动,需要自己做对应的事件处理:

dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()

但是在5.0之后google为我们提供了NestedScrolling机制,包含如下四个方法(support.v4为大家提供了:NestedScrollingParent、NestedScrollingChild):

NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper

关于事件分发的介绍-part one:

  • Touch事件分发的主角只有两个:ViewGroup、View,Activity的touch事件实际上是调用内部的ViewGroup的Touch事件,可以直接当成ViewGroup处理。

  • View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候把内部的ViewGroup当成View来分析。

  • ViewGroup的相关事件有三个:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。View的相关事件只有两个:dispatchTouchEvent、onTouchEvent。Activity的相关事件有两个:dispatchTouchEvent、onTouchEvent。

先分析 ViewGroup的处理流程:首先得有个结构模型概念:ViewGroup和View组成了一棵树形结构,最顶层为Activity的 ViewGroup,下面有若干的ViewGroup节点,每个节点之下又有若干的ViewGroup节点或者View节点,依次类推。如图:

Android NestedScrolling(嵌套滑动)机制_第1张图片
常见的典型touch事件结构图

当一个Touch事件(触摸事件)到达根结点,即activity的ViewGroup时,它会依次下发,下发的过程调用子View或者Viewgroup的dispatchTouchEvent方法。简单来说,就是ViewGroup会遍历它包含的子View,调用每个View的dispatchTouchEvent,当遇到ViewGroup时,又会调用ViewGroup的dispatchTouchEvent方法,继续调用其内部的View的dispatchTouchEvent方法,上图中的调用顺序为:1->2->5->6->7->3->4 dispatchTouchEvent方法只负责事件的分发,它拥有boolean类型的返回值,当返回为true时, 顺序下发会中断。在上述例子中如果5的dispatchTouchEvent返回结果为true,那么6->7->3->4将都接收不到本次Touch事件。

关于ViewGroup中dispatchTouchEvent简单分析:

/*** ViewGroup
 * @param ev
 * @return
 */
public boolean dispatchTouchEvent(MotionEvent ev){
    ....//其他处理,在此不管
    View[] views=getChildView();
    for(int i=0;i

由此可见,ViewGroup中的dispatchTouchEvent是真正执行“分发”工作,而View中对应的dispatchTouchEvent并不执行分发工作,或者说它分发的对象就是他自己,决定是否把touch事件交给自己处理,而处理的方法,便是onTouchEvent事件,事实上子View的dispatchTouchEvent方法真正执行的代码是这样的

/*** View
 * @param ev
 * @return
 */
public boolean dispatchTouchEvent(MotionEvent ev){
    ....//其他处理,在此不管
    return onTouchEvent(event);
}

一般情况下,我们不该在普通View内重写dispatchTouchEvent方法,因为它并不执行分发逻辑。当Touch事件到达View时,我们该做的就是是否在onTouchEvent事件中处理它。

那么,ViewGroup的onTouchEvent事件是什么时候处理的呢?当ViewGroup所有的子View都返回false时,onTouchEvent事件便会执行。由于ViewGroup是继承于View的,它其实也是通过调用View的dispatchTouchEvent方法来执行onTouchEvent事件。在目前的情况看来,似乎只要我们把所有的onTouchEvent都返回false,就能保证所有的子控件都响应本次Touch事件了。但必须要说明的是,这里的Touch事件,只限于Acition_Down事件,即触摸按下事件,而Aciton_UP和Action_MOVE却不会执行。事实上,一次完整的Touch事件,应该是由一个Down、一个Up和若干个Move组成的。Down方式通过dispatchTouchEvent分发,分发的目的是为了找到真正需要处理完整Touch请求的View。当某个View或者ViewGroup的onTouchEvent事件返回true时,便表示它是真正要处理这次请求的View,之后的Aciton_UP和Action_MOVE将由它处理。当所有子View的onTouchEvent都返回false时,这次的Touch请求就由根ViewGroup,即Activity自己处理了。

View mTarget=null;//保存捕获Touch事件处理的View
public boolean dispatchTouchEvent(MotionEvent ev) {
    //....其他处理,在此不管
    if(ev.getAction()==KeyEvent.ACTION_DOWN){
        //每次Down事件,都置为Null
       if(!onInterceptTouchEvent()){ 
        mTarget=null;
        View[] views=getChildView();
        for(int i=0;i

ViewGroup还有个onInterceptTouchEvent,看名字便知道这是个拦截事件。这个拦截事件需要分两种情况来说明:

1.假如我们在某个ViewGroup的onInterceptTouchEvent中,将Action为Down的Touch事件返回true,那便表示将该ViewGroup的所有下发操作拦截掉,这种情况下,mTarget会一直为null,因为mTarget是在Down事件中赋值的。由于mTarge为null,该ViewGroup的onTouchEvent事件被执行。这种情况下可以把这个ViewGroup直接当成View来对待。
2.假如我们在某个ViewGroup的onInterceptTouchEvent中,将Acion为Down的Touch事件都返回false,其他的都返回True,这种情况下,Down事件能正常分发,若子View都返回false,那mTarget还是为空,无影响。若某个子View返回了true,mTarget被赋值了,在Action_Move和Aciton_UP分发到该ViewGroup时,便会给mTarget分发一个Action_Delete的MotionEvent,同时清空mTarget的值,使得接下去的Action_Move(如果上一个操作不是UP)将由ViewGroup的onTouchEvent处理。情况一用到的比较多,情况二个人还未找到使用场景。

从头到尾总结一下:

  • Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。

  • ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。

  • 触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。

  • 当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。

  • 当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。

  • 当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用 super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。

  • onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

补充:“触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。”,这里补充下其实UP事件是可能为0个的。

对于onInterceptTouchEvent事件,它的应用场景在很多带scroll效果的ViewGroup中都有体现。设想一下再一个ViewPager中,每个Item都是个ImageView,我们需要对这些ImageView做Matrix操作,这不可避免要捕获掉Touch事件,但是我们又需要做到不影响ViewPager翻页效果,这又必须保证ViewPager能捕获到Move事件,于是,ViewPager的onInterceptTouchEvent会对Move事件做一个过滤,当适当条件的Move事件(持续若干事件或移动若干距离,这里我没读源码只是猜测)触发时,并会拦截掉,返回子View一个Action_Cancel事件。这个时候子View就没有Up事件了,很多需要在Up中处理的事物要转到Cancel中处理。

单个控件的事件分发流程图如下:(以Button为例)

Android NestedScrolling(嵌套滑动)机制_第2张图片
button 触摸事件的处理

因此,事件分发之间的关系是:dispatchTouchEvent方法中先执行 onTouch接口回调,然后根据onTouch方法的返回值判断是否执行onTouchEvent方法,onTouchEvent方法中执行了onClick接口回调。
在ViewGroup中onInterceptTouchEvent返回true时,才会调用自己的onTouchEvent,dispatchTouchEvent返回true,不会调用自己的onTouchEvent。

结论:dispatchTouchEvent---->onTouch---->onTouchEvent----->onClick。并且如果仔细的你会发现,是在**所有ACTION_UP事件之后才触发onClick点击事件。 **

关于事件分发的介绍-part two:

Android中默认情况下事件传递是由最终的view的接收到,传递过程是从父布局到子布局,也就是从Activity到ViewGroup到View的过程,默认情况,ViewGroup起到的是透传作用。Android中事件传递过程(按箭头方向)如下图:

Android NestedScrolling(嵌套滑动)机制_第3张图片
触摸事件整体分析

触摸事件是一连串ACTION_DOWN,ACTION_MOVE..MOVE…MOVE、最后ACTION_UP,触摸事件还有ACTION_CANCEL事件。事件都是从ACTION_DOWN开始的,Activity的dispatchTouchEvent()首先接收到ACTION_DOWN,执行super.dispatchTouchEvent(ev),事件向下分发。
dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。

Android NestedScrolling(嵌套滑动)机制_第4张图片
ACTION_DOWN都没被消费
Android NestedScrolling(嵌套滑动)机制_第5张图片
ACTION_DOWN被View消费了
Android NestedScrolling(嵌套滑动)机制_第6张图片
后续ACTION_MOVE和UP在不被拦截的情况下都会去找VIEW
Android NestedScrolling(嵌套滑动)机制_第7张图片
后续的被拦截了
Android NestedScrolling(嵌套滑动)机制_第8张图片
ACTION_DOWN一开始就被拦截

android中的Touch事件都是从ACTION_DOWN开始的:

**单手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP
多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOVE--ACTION_POINTER_UP---ACTION_UP
**

关于嵌套滑动机制:

/**
 * 重点关注方法:
 * onStartNestedScroll
 * onNestedPreScroll
 */
public class StickyNavLayout extends LinearLayout implements NestedScrollingParent {
    private static final String TAG = "StickyNavLayout";

    /**
     * onStartNestedScroll该方法返回true,代表当前ViewGroup能接受内部View的滑动参数(这个内部View不一定是直接子View),
     * 一般情况下建议直接返回true,当然你可以根据nestedScrollAxes:判断垂直或水平方向才返回true。
     */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.e(TAG, "onStartNestedScroll");
        return true;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        Log.e(TAG, "onNestedScrollAccepted");
    }

    @Override
    public void onStopNestedScroll(View 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);
        Log.d("onNestedPreScroll", "--------------------------------------------------------------------------------");
        Log.d("onNestedPreScroll", "ViewCompat.canScrollVertically(target, -1) target view 向下滚动--->:" + ViewCompat.canScrollVertically(target, -1));
        Log.d("onNestedPreScroll", "!ViewCompat.canScrollVertically(target, -1) --->:" + !ViewCompat.canScrollVertically(target, -1));
        Log.d("onNestedPreScroll", "ViewCompat.canScrollVertically(target, 1) target  view 向上滚动--->:" + ViewCompat.canScrollVertically(target, 1));
        Log.d("onNestedPreScroll", "!ViewCompat.canScrollVertically(target, 1)--->:" + !ViewCompat.canScrollVertically(target, 1));
        Log.d("onNestedPreScroll", "hiddenTop ->" + String.valueOf(hiddenTop));
        Log.d("onNestedPreScroll", "showTop ->" + String.valueOf(showTop));
        Log.d("onNestedPreScroll", "dy/2 ->" + String.valueOf(dy / 2));
        Log.d("onNestedPreScroll", "--------------------------------------------------------------------------------");
        if (hiddenTop || showTop) {
            /**
             * 这个是top 每次scroll的距离
             */
            scrollBy(0, dy);
            /**
             * 这个是top消耗的距离
             * 如果为dy/2,则余下的dy/2由子view消耗,即滑动的过程中,子view同时也会滑动
             */
            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 0;
    }

    private View mTop;
    private View mNav;
    private ViewPager mViewPager;

    private int mTopViewHeight;

    private OverScroller mScroller;
    private VelocityTracker mVelocityTracker;
    private int mTouchSlop;
    private int mMaximumVelocity, mMinimumVelocity;

    private float mLastY;
    private boolean mDragging;

    public StickyNavLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOrientation(LinearLayout.VERTICAL);

        mScroller = new OverScroller(context);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context)
                .getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context)
                .getScaledMinimumFlingVelocity();

    }

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    @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();
    }

    /**
     * 对于fling方法,我们使用OverScroller的fling方法,另外边界检测,重写了scrollTo方法:
     */
    public void fling(int velocityY) {
        mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
        invalidate();   
    }

    @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();
        }
    }
}

你可能感兴趣的:(Android NestedScrolling(嵌套滑动)机制)