美文网首页Android开发学习Android相关UI效果
Android嵌套滑动机制源码分析

Android嵌套滑动机制源码分析

作者: 菜鸟的姿态 | 来源:发表于2017-05-03 21:43 被阅读303次

    Android在发布 5.0(Lollipop)版本之后,Google为我们提供了嵌套滑动的特性。下面,我们从源码角度去分析Android嵌套滑动的实现机制。

    首先,我们先来看一下以下嵌套滑动相关的4个核心类的实现:

    NestedScrollingChild

    NestedScrollingChildHelper

    NestedScrollingParent

    NestedScrollingParentHelper


    NestedScrollingChild

    package android.support.v4.view;
    
    import android.view.MotionEvent;
    import android.view.VelocityTracker;
    import android.view.View;
    import android.view.ViewConfiguration;
    import android.view.ViewParent;
    
    public interface NestedScrollingChild {
        /**
         * 设置是否允许嵌套滑动
         *
         * @param enabled
         */
        public void setNestedScrollingEnabled(boolean enabled);
    
        /**
         * 判断是否允许嵌套滑动
         * 
         * @return true if nested scrolling is enabled
         */
        public boolean isNestedScrollingEnabled();
    
        /**
         * 开始嵌套滑动
         *
         * @param axes 表示滑动方向(3个可能值)
         *             ViewCompat#SCROLL_AXIS_HORIZONTAL
         *             ViewCompat#SCROLL_AXIS_VERTICAL
         *             ViewCompat#SCROLL_AXIS_HORIZONTAL | ViewCompat#SCROLL_AXIS_VERTICAL
         * @return true if a cooperative parent was found and nested scrolling has been enabled for the current gesture.
         */
        public boolean startNestedScroll(int axes);
    
        /**
         * 停止嵌套滑动
         */
        public void stopNestedScroll();
    
        /**
         * 判断是否有父类支持嵌套滑动
         */
        public boolean hasNestedScrollingParent();
    
       /**
         * ChildView执行完scroll后调用,通知ParentView其实际的滑动距离和未消耗的滑动距离
         *
         * @param dxConsumed ChildView在x轴实际滑动的距离
         * @param dyConsumed ChildView在y轴实际滑动的距离
         * @param dxUnconsumed ChildView在x轴未消耗的距离(一般ParentView根据此数据处理自身的x轴滑动)
         * @param dyUnconsumed ChildView在y轴未消耗的距离(一般ParentView根据此数据处理自身的y轴滑动)
         * @param offsetInWindow ChildView的窗体偏移量
         */
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
    
       /**
         * ChildView执行scroll前,用于通知ParentView意图滑动的距离(onInterceptTouchEvent或者onTouch中调用)
         *
         * @param dx ChildView在x轴上意图滑动的距离
         * @param dy ChildView在y轴上意图滑动的距离
         * @param consumed 输出参数,记录ParentView消费的滑动距离(x轴:consumed[0],y轴:consumed[1])
         * @param offsetInWindow ChildView的窗体偏移量
         */
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
    
       /**
         * ChildView进行fling滑动时调用,通知ParentView
         *
         * @param velocityX ChildView在x轴的fling速率
         * @param velocityY ChildView在y轴的fling速率
         * @param consumed ParentView是否消费本次fling操作
         * @return true if the nested scrolling parent consumed or otherwise reacted to the fling
         */
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
        
       /**
         * ChildView进行fling操作前调用,通知ParentView其fling的速率
         *
         * @param velocityX ChildView在x轴的fling速率
         * @param velocityY ChildView在y轴的fling速率
         * @return true if a nested scrolling parent consumed the fling
         */
        public boolean dispatchNestedPreFling(float velocityX, float velocityY);
    }
    

    以上NestedScrollingChild接口的方法描述已经比较详细,实现嵌套滑动时,ChildView必须实现该接口。通过查看RecycleView源码,我们可以知道NestedScrollingChild的接口实现完全由NestedScrollingChildHelper代理实现。因此,我们接下来分析以下NestedScrollingChildHelper的代码实现!


    NestedScrollingChildHelper

    package android.support.v4.view;
    
    import android.view.View;
    import android.view.ViewParent;
        
    public class NestedScrollingChildHelper {
    
        /**
         * Helper对应的ChildView
         */
        private final View mView;
        
        /**
         * A nested scrolling parent view currently receiving events for a nested scroll in progress.
         */
        private ViewParent mNestedScrollingParent;
        
        /**
         * 是否允许嵌套滑动
         */ 
        private boolean mIsNestedScrollingEnabled;
        
       /**
         * 记录上次scroll event中ParentView消耗的滑动距离
         */
        private int[] mTempNestedScrollConsumed;
    
        public NestedScrollingChildHelper(View view) {
            mView = view;
        }
    
        /**
         * 设置是否允许嵌套滑动
         *
         * @param enabled true to enable nested scrolling dispatch from this view, false otherwise
         */
        public void setNestedScrollingEnabled(boolean enabled) {
            if (mIsNestedScrollingEnabled) {
                ViewCompat.stopNestedScroll(mView);
            }
            mIsNestedScrollingEnabled = enabled;
        }
    
        /**
         * 判断是否允许嵌套滑动
         *
         * @return true if nested scrolling is enabled for this view
         */
        public boolean isNestedScrollingEnabled() {
            return mIsNestedScrollingEnabled;
        }
    
        /**
         * 判断ChildView此时是否存在一个接收嵌套滑动事件的ParentView
         *
         * @return true if this view has a nested scrolling parent, false otherwise
         */
        public boolean hasNestedScrollingParent() {
            return mNestedScrollingParent != null;
        }
    
        /**
         * ChildView接收到滑动请求时调用
         *(查找符合要求的Nested Scrolling ParentView)
         *
         * @param axes ChildView的滑动方向
         * @return true if a cooperating parent view was found and nested scrolling started successfully
         */
        public boolean startNestedScroll(int axes) {
            if (hasNestedScrollingParent()) {
                // 目前处于嵌套滑动处理阶段
                return true;
            }
            if (isNestedScrollingEnabled()) {
                // 接受嵌套滑动的ParentView
                ViewParent p = mView.getParent();
                // ParentView的直接子控件:ChildView本身或包含ChildView
                View child = mView;
                while (p != null) {
                    if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                        mNestedScrollingParent = p;
                        // 找到符合要求的Nested Scrolling ParentView时,通知ParentView准备
                        ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                        return true;
                    }
                    if (p instanceof View) {
                        child = (View) p;
                    }
                    p = p.getParent();
                }
            }
            return false;
        }
    
        /**
         * 停止当前的嵌套滑动
         */
        public void stopNestedScroll() {
            if (mNestedScrollingParent != null) {
                // 通知ParentView停止嵌套滑动
                ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
                mNestedScrollingParent = null;
            }
        }
    
        /**
         * ChildView执行完scroll操作后,通知ParentView
         *(参考NestedScrollingChild#dispatchNestedScroll函数的注释说明)
         */
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                    int startX = 0;
                    int startY = 0;
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        startX = offsetInWindow[0];
                        startY = offsetInWindow[1];
                    }
    
                   // 通知ParentView,ChildView的实际滑动情况(ParentView一般根据dxUnconsumed| dyUnconsumed处理自身滑动)
                    ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                            dyConsumed, dxUnconsumed, dyUnconsumed);
    
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                    }
                    return true;
                } else if (offsetInWindow != null) {
                    // No motion, no dispatch. Keep offsetInWindow up to date.
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }
    
        /**
         * ChildView执行scroll前,通知Parent其意图滑动的距离
         *(参考NestedScrollingChild#dispatchNestedPreScroll函数注释说明)
         */
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                if (dx != 0 || dy != 0) {
                    int startX = 0;
                    int startY = 0;
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        startX = offsetInWindow[0];
                        startY = offsetInWindow[1];
                    }
    
                    if (consumed == null) {
                        if (mTempNestedScrollConsumed == null) {
                            mTempNestedScrollConsumed = new int[2];
                        }
                        consumed = mTempNestedScrollConsumed;
                    }
                    consumed[0] = 0;
                    consumed[1] = 0;
                    ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
    
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                    }
                    return consumed[0] != 0 || consumed[1] != 0;
                } else if (offsetInWindow != null) {
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }
    
        /**
         * ChildView执行fling滑动操作后,通知ParentView
         * (参考NestedScrollingChild#dispatchNestedFling函数)
         */
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
                        velocityY, consumed);
            }
            return false;
        }
    
        /**
         * ChildView执行fling滑动操作前,通知ParentView,由ParentView确定是否消费fling滑动事件
         * (参考NestedScrollingChild#dispatchNestedPreFling函数)
         *   
         * @return arentView是否消费fling滑动事件
         */
        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
                        velocityY);
            }
            return false;
        }
    
        /**
         * 在ChildView的onDetachedFromWindow回调函数中调用,结束嵌套滑动
         */
        public void onDetachedFromWindow() {
            ViewCompat.stopNestedScroll(mView);
        }
    
        /**
         * 停止嵌套滑动
         *(nested scroll ChildView停止本次嵌套滑动时,处理ChildView自身的状态)
         */
        public void onStopNestedScroll(View child) {
            ViewCompat.stopNestedScroll(mView);
        }
    }
    

    NestedScrollingChildHelper可以说是嵌套滑动机制中最重要的类,ChildView实现NestedScrollingChild接口时,所有NestedScrollingChild的函数实现都交给NestedScrollingChildHelper代理实现,包括:查找接受嵌套滑动的ParentView,通知ParentView嵌套滑动的滑动距离、方向,通知ParentView结束嵌套滑动等。


    NestedScrollingParent

    package android.support.v4.view;
    
    import android.view.MotionEvent;
    import android.view.VelocityTracker;
    import android.view.View;
    import android.view.ViewConfiguration;
    
    /**
     * 实现嵌套滑动的ParentView必须实现该接口
     */
    public interface NestedScrollingParent {
        /**
         * ParentView判断是否进行嵌套滑动
         *
         * @param child  ParentView的直接子控件(包含target)
         * @param target 嵌套滑动的ChildView
         * @param nestedScrollAxes ChildView的嵌套滑动方向
         * @return true if this ViewParent accepts the nested scroll operation
         */
        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    
        /**
         * 当onStartNestedScroll返回true时调用
         *(一般直接调用NestedScrollingParentHelper#onNestedScrollAccepted,记录嵌套滑动方向)
         */
        public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    
        /**
         * ParentView结束嵌套滑动
         *(一般直接调用NestedScrollingParentHelper#onStopNestedScroll,重置嵌套滑动方向flag为0)
         */
        public void onStopNestedScroll(View target);
    
        /**
         * 接收ChildView的滑动通知(ChildView scroll后)
         *(当ChildView调用dispatchNestedScroll函数时间接调用,一般ParentView在函数中实现滑动操作)
         */
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed);
    
        /**
         * 接收ChildView的滑动通知(ChildView scroll前)
         *(当ChildView调用 dispatchNestedPreScroll函数时间接调用,由ParentView决定自身消耗的滑动距离)
         */
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    
        /**
         * 类似onNestedScroll,只是针对fling操作
         */
        public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    
        /**
         * 类似onNestedPreScroll,只是针对fling操作
         */
        public boolean onNestedPreFling(View target, float velocityX, float velocityY);
    
        /**
         * 返回嵌套滑动的方向
         * (直接调用NestedScrollingParentHelper#getNestedScrollAxes)
         */
        public int getNestedScrollAxes();
    }
    

    实现嵌套滑动的ParentView必须实现NestedScrollingParent接口类,用于判断是否接受嵌套滑动、计算ParentView消耗的滑动距离以及接收到ChildView的滑动通知后,ParentView本身进行滑动等逻辑。以上函数都不是ParentView自身直接调用,而是由ChildView实现NestedScrollingChild接口的方法间接调用!


    NestedScrollingParentHelper

    public class NestedScrollingParentHelper {
    
        /**
         * 实现嵌套滑动的ParentView
         */
        private final ViewGroup mViewGroup;
        
       /**
         * 记录嵌套滑动的方向flag
         */
        private int mNestedScrollAxes;
    
        public NestedScrollingParentHelper(ViewGroup viewGroup) {
            mViewGroup = viewGroup;
        }
    
        /**
         * 返回嵌套滑动的方向flag
         */
        public int getNestedScrollAxes() {
            return mNestedScrollAxes;
        }
    
        /**
         * 当ParentView接收嵌套滑动操作时调用
         * (在NestedScrollingParent#onNestedScrollAccepted接口实现中调用)
         */
        public void onNestedScrollAccepted(View child, View target, int axes) {
            // 记录嵌套滑动方向
             mNestedScrollAxes = axes;
        }
        
        /**
         * 当嵌套滑动结束时调用
         * (在NestedScrollingParent#onStopNestedScroll接口实现中调用)
         */
        public void onStopNestedScroll(View target) {
            // 重置嵌套滑动方向
            mNestedScrollAxes = 0;
        }
    }
    

    以上NestedScrollingParentHelper的实现比较简单,基本都是在NestedScrollingParent的对应接口调用,只是在ParentView接受嵌套滑动和结束嵌套滑动时,改变记录嵌套滑动方向flag的值mNestedScrollAxes。


    建议熟悉以上4个类后,查看一下RecycleView中NestedScrollingChild接口的实现,以及调用NestedScrollingChild接口的时机;查看一下NestedScrollView中NestedScrollingParent接口的实现,即可对嵌套滑动机制大致掌握!

    下一篇,我将通过简单分析RecycleView和NestedScrollView源码,以及实现简单的demo,帮助大家更深刻地了解嵌套滑动机制!

    总结一下,嵌套滑动的工作流程如下:

    (1)ChildView接收到TouchEvent(down类型)后,先查找符合嵌套滑动的ParentView :

    NestedScrollingChild#startNestedScroll(int axes) -》 
    NestedScrollingChildHelper#startNestedScroll(int axes) -》
    递归查找符合嵌套滑动要求的ParentView(即NestedScrollingParent#startNestedScroll(int axes)返回true的父控件)
    

    (2)ChildView(实现NestedScrollingChild接口)接收到滑动请求时,先通知ParentView(实现NestedScrollingParent接口)其意图滑动的方向nestedScrollAxes和距离dx & dy:

    // consumed作为输出参数,获取ParentView的消耗滑动距离
    NestedScrollingChild#dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
    

    (3)ParentView可以根据自定义规则,计算需要消耗滑动距离int[] consumed,再通知ChildView;

    //consumed作为输出参数,记录ParentView的消耗滑动距离
    NestedScrollingParent#onNestedPreScroll(View target, int dx, int dy, int[] consumed)
    

    (4)ChildView根据ParentView返回的数据int[] consumed,重新计算需要滑动的距离dxConsumed & dyConsumed以及不消耗的滑动距离dxUnconsumed & dyUnconsumed;

    (5)ChildView根据步骤4计算得到实际的滑动距离后,先自身调用scroll函数进行滑动,然后通知ParentView其滑动情况;

    NestedScrollingChild#dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
    

    (6)ParentView接收到ChildView的实际滑动情况时,可以根据dxUnconsumed & dyUnconsumed,确定ParentView自身的滑动。

    NestedScrollingParent#onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
    

    注意:

    • 仅当步骤(1)中找到接受嵌套滑动的parentView,才会进行后面的步骤;

    • 以上步骤(2)~(6)为循环步骤,在同一系列的Touch Events事件流中,每一个move类型的Touch Event都会触发一次(2)~(6)的组合步骤,从而可以根据手指在屏幕的触摸动态处理ChildView和ParentView的滑动!

    • 直至,ChildView调用NestedScrollingChild#stopNestedPreScroll函数(一般为接收到类型up或cancel的Touch Event时调用),则嵌套滑动结束。


    参考链接:
    NestedScrolling事件机制源码解析

    相关文章

      网友评论

      本文标题:Android嵌套滑动机制源码分析

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