NestedScrolling 机制深入解析 雨点打透心脏的1/2处 2022-06-16 08:19 216阅读 0赞 [使用CoordinatorLayout打造各种炫酷的效果][CoordinatorLayout] [自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示][Behavior _ _FloatActionButton] [NestedScrolling 机制深入解析][NestedScrolling] [ 一步步带你读懂 CoordinatorLayout 源码][_ CoordinatorLayout] [自定义 Behavior ——仿新浪微博发现页的实现][Behavior] ![仿新浪微博效果图][strip] **简介** NestedScrolling,在 V4 包下面,在 22.10 版本的时候添加进来,支持 5.0 及 5.0 以上的系统。 NestedScrolling,简称嵌套滑动使用它可以实现一些非常绚丽的效果。如知乎的效果,UC 首页的效果,新浪微博发现的效果等。 Google 帮我们封装好了一些相应的空间,比如 RecyclerView 实现了 NestedScrollingChild 接口,CoordinatorLayout 实现了 NestedScrollingParent 接口,NestedScrollingView,SwipeRefreshLayout 实现了 NestedScrollingChild,NestedScrollingParent 接口等。 **想比较于传统的事件分发机制,NetstedScroll 机制有什么优点,相信很多人都有这样的疑问?**。 在传统的事件分发机制 中,一旦某个 View 或者 ViewGroup 消费了事件,就很难将事件交给父 View 进行共同处理。而 NestedScrolling 机制很好地帮助我们解决了这一问题。我们只需要按照规范实现相应的接口即可,子 View 实现 NestedScrollingChild,父 View 实现 NestedScrollingParent ,通过 NestedScrollingChildHelper 或者 NestedScrollingParentHelper 完成交互。 ## **NestedScrolling 机制简述** ## ### NestedScrolling 的处理流程 ### NestedScrolling 机制主要有两个类, * NestedScrollingParent 在嵌套滑动中,如果父View 想实现 嵌套滑动,要实现这个 NestedScrollingParent 借口,与 NestedScrollingChild 大概有一一对应的关系。 * NestedScrollingChild 在嵌套滑动中,如果scrolling child 想实现嵌套滑动,必须实现这个借口 * NestedScrollingChildHelper 实现 Child 和 Parent 交互的逻辑 * NestedScrollingParentHelper 实现 Child 和 Parent 交互的逻辑 它的处理流程大概是这样的: ![9fe4afa0gy1ffg6uv3ucfj20lh0bjwfe.jpg][] * scrolling child 在滑动之前,会通过 NestedScrollingChildHelper 查找是否有响应的 scrolling parent,如果有的话,会先询问scrolling parent 是否需要先于scrolling child 滑动,如果需要的话,scrolling parent 进行相应的滑动,并消费一定的距离; * 接着scrolling child 进行相应的滑动,并消耗一定的距离值 dx,dy * scrolling child 滑动完之后,询问scrolling parent 是否还需要继续进行滑动,需要的话,进行相应的处理。 * 滑动结束之后,Scrolling child 会停止滑动,并通过 NestedScrollingChildHelper 通知相应的 Scrolling Parent 停止滑动。 **为了方便,下文开始,ScrollingChildHelper 用 childHelper 代替,NestedScrollingParentHelper 用parentHelper 代替** -------------------- ## **NestedScrollingChild 主要方法介绍** ## 目前已知的实现子类有 HorizontalGridView, NestedScrollView, RecyclerView, SwipeRefreshLayout, VerticalGridView * boolean startNestedScroll(int axes) 在开始滑动的时候会调用这个方法,axes 代表滑动的方向,ViewCompat.SCROLL\_AXIS\_HORIZONTAL 代表水平滑动,ViewCompat.SCROLL\_AXIS\_VERTICAL 代表垂直滑动, 返回值是布尔类型的,根据返回值,我们可以判断是否找到支持嵌套滑动的父View ,返回 true,表示在scrolling parent (需要注意的是这里不一定是直接scrolling parent ,间接scrolling parent 也可会返回 TRUE) 中找到支持嵌套滑动的。反之,则找不到。 * boolean dispatchNestedPreScroll(int dx, int dy, int\[\] consumed, int\[\] offsetInWindow) 在scrolling child 滑动之前,提供机会让scrolling parent 先于scrolling child滑动。 dx,dy 是输入参数,表示scrolling child 传递给 scrolling parent 水平方向,垂直方向上的偏移量,consumed 是输出参数,consumed\[0\] 表示父 View 在水平方向上消费的值,,consumed\[1 表示父 View 在垂直方向上消费的值。 返回值也是布尔类型的,根据这个值 ,我们可以判断scrolling parent 是都消费了相应距离 。 * boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int\[\] offsetInWindow) 在scrolling child 滑动之后,调用这个方法,提供机会给scrolling parent 滑动,dxConsumed,dyConsumed 是输入参数,表示scrolling child 在水平方向,垂直方向消耗的值,dxUnconsumed,dyUnconsumed 也是输入参数,表示scrolling child 在水平方向,垂直方向未消耗的值。 * boolean dispatchNestedPreFling(float velocityX, float velocityY, boolean consumed) 调用这个方法,在scrolling child 处理 fling 动作之前,提供机会scrolling parent 先于scrolling child 处理 fling 动作。 三个参数都是输入参数,velocityX 表示水平方向的速度,velocityY 表示垂直方向感的速度,consumed 表示scrolling child 是否消费 fling 动作 。 返回值也是布尔类型的,表示scrolling parent 是否有消费了fling 动作或者对 fling 动作做出相应的 处理。true 表示有,false 表示没有。 * boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) 在 Scrolling child 处理 fling 动作之后,提供机会给 Scrolling Parent 处理 fling 动作。各个参数的意义这里就不再意义阐述了,跟 dispatchNestedFling 参数的意义是一样的。 * void stopNestedScroll 当嵌套滑动的时候,会调用这个方法。 在 RecyclerView 中,当 Action\_UP 或者 Actioon\_cancel 或者 item 消费了 Touch 事件的时候,会调用这个方法。 -------------------- ## **NestedScrollingParent** ## Android 中已知的实现子类有 CoordinatorLayout, NestedScrollView, SwipeRefreshLayout。它通常是配合 NestedScrollingChild 进行嵌套滑动的。 * boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) 在 Scrolling Child 开始滑动的时候会调用这个方法 当 Scrolling Child 调用 onStartNestedScroll 方法的时候,通过 NestedScrollingChildHelper 会回调 Scrolling parent 的 onStartNestedScroll 方法,如果返回 true, Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 方法会被回调。 target 表示发起滑动事件的 View,Child 是 ViewParent 的直接子View,包含 target,nestedScrollAxes 表示滑动方向。 * void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 如果 Scrolling Parent 的onStartNestedScroll 返回 true, Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 方法会被回调。 * boolean onNestedPreScroll(View target, int dx, int dy, int\[\] consumed) 在 Scrolling Child 进行滑动之前,Scrolling Parent 可以先于Scrolling Child 进行相应的处理 如果 Scrolling Child 调用 dispatchNestedPreFling(float velocityX, float velocityY) ,通过 NestedScrollingChildHelper 会回调 Scrolling parent 的 onNestedPreScroll 方法 接下来的几个方法,我们不一一介绍了。与 Scrolling Child 方法几乎是一一对应的。 ## **NetsedScrollingchildHelper 与 NestedScrollingParentHelper** ## 我们知道 RecyclerView 是实现了 NestedScrollingChild 接口,下面我们一起来看一下RecyclerView 是怎样将事件传递给 Scrolling Parent 的。 public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild private NestedScrollingChildHelper getScrollingChildHelper() { if (mScrollingChildHelper == null) { mScrollingChildHelper = new NestedScrollingChildHelper(this); } return mScrollingChildHelper; } @Override public void setNestedScrollingEnabled(boolean enabled) { getScrollingChildHelper().setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return getScrollingChildHelper().isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return getScrollingChildHelper().startNestedScroll(axes); } @Override public void stopNestedScroll() { getScrollingChildHelper().stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return getScrollingChildHelper().hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); } 从代码中可以看到,它的很多逻辑都是交给 ChildHelper 去帮助 其完成的,下面我们一起来看一下 ChildHelper 里面的方法。 ### startNestedScroll 方法 ### public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } // 判断是否支持嵌套滑动,默认是支持的 if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; // 从直接父 View 找起,看是否支持嵌套滑动 while (p != null) { // //回调了父View的onStartNestedScroll方法 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() p = p.getParent(); } } return false; } * 第一步,判断 P 是否为空,不为空, 从 P (初始值是RecyclerView 的直接父 View) 开始找起,判断其是否支持嵌套滑动,若支持,返回true, * 第二步:若 P 不支持嵌套滑动,再将 p 指向 p.getParent(); 循环第一步 * 第三步:若循环了所有的 P ,都找不到支持嵌套滑动的 View,返回 false。 ### 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); // 保存刚开始 x 在 window 坐标系的偏移量 startX = offsetInWindow[0]; // 保存刚开始 y 方向在 window 坐标系的偏移量 startY = offsetInWindow[1]; } // 调用 mNestedScrollingParent 的 onNestedScroll 方法 ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); // offsetInWindow 不为空 if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); // 得到 x 方向在 Window 坐标系的偏移量 offsetInWindow[0] -= startX; // 得到 y 方向在 Window 坐标系的偏移量 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; } 简单来说就是根据上一步在 startScrolled 方法中得到的 mNestedScrollingParent,调用 ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed,dyUnconsumed);再根据是否有位移,做相应的处理。 看完了上面的两个主要方法,我们可以得出这样的一个结论:当我们调用 Scrolling Child 的 onStartNested 方法的时候,会通过 ChildHelper 去寻找是否有相应的 Scrolling Parent,如果有的话,会 回调相应的方法。同理 dispatchNestedPreScroll,dispatchNestedScroll,dispatchNestedPreFling 也是如此,这里不再一一带大家去看里面是怎样实现的,有兴趣的可以自己去阅读。 -------------------- ## startNestedScroll ,dispatchNestedPreScroll 等方法的调用时机 ## 这里我们同样以 RecyclerView 为例讲解:在 OnTouchEvent 方法里面,可以看到会根据不同的 Action 回调不同的方法,这里就不一一阐述了,回调方法的 事件请看代码。 public boolean onTouchEvent(MotionEvent e) { --- // 如果 Item 处理了 Touch 事件,直接返回 true ,在在处理 if (dispatchOnItemTouch(e)) { cancelTouch(); return true; } if (mLayout == null) { return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } boolean eventAddedToVelocityTracker = false; ------- switch (action) { case MotionEvent.ACTION_DOWN: { mScrollPointerId = e.getPointerId(0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontally) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertically) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } // 在 Action_Down 的时候 调用 startNestedScroll startNestedScroll(nestedScrollAxis); } break; ---- case MotionEvent.ACTION_MOVE: { // 在 Action_move 的时候,回调 dispatchNestedPreScroll 方法 if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { // 减去 Scrolling Parent 的消费的值 dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); // Updated the nested offsets mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; } ---- if (mScrollState == SCROLL_STATE_DRAGGING) { mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1]; // 在 scrollByInternal 方法里面会回调 onNestedScroll 方法 if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } if (mGapWorker != null && (dx != 0 || dy != 0)) { mGapWorker.postFromTraversal(this, dx, dy); } } } break; case MotionEvent.ACTION_UP: { --- // 在 fling 方法里面会回调 onNestedPreFling dispatchNestedFling 等方法 if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } // 在手指抬起的时候回调 onStopScroll 方法 resetTouch(); } break; case MotionEvent.ACTION_CANCEL: { // 在 ACTION_CANCEL 的时候回调 onStopScroll 方法 cancelTouch(); } break; } if (!eventAddedToVelocityTracker) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; } private void resetTouch() { if (mVelocityTracker != null) { mVelocityTracker.clear(); } stopNestedScroll(); releaseGlows(); } private void cancelTouch() { resetTouch(); setScrollState(SCROLL_STATE_IDLE); } -------------------- ## 总结 ## ### NestedScrollingChild 与 NestedScrollingParent 方法的对应关系 ### <table> <thead> <tr> <th>子View</th> <th>父View</th> <th>方法描述</th> </tr> </thead> <tbody> <tr> <td>startNestedScroll</td> <td>onStartNestedScroll、onNestedScrollAccepted</td> <td>Scrolling Child 开始滑动的时候,通知 Scrolling Parent 要开始滑动了,通常是在 Action_down 动作 的时候调用这个方法</td> </tr> <tr> <td>dispatchNestedPreScroll</td> <td>onNestedPreScroll</td> <td>在 Scrolling Child 要开始滑动的时候,询问 Scrolling Parent 是否先于 Scrolling Child 进行相应的处理,同时是在 Action_move 的时候调用</td> </tr> <tr> <td>dispatchNestedScroll</td> <td>onNestedScroll</td> <td>在 Scrolling Child 滑动后会询问 Scrolling Parent 是否需要继续滑动</td> </tr> <tr> <td>dispatchNestedPreFling</td> <td>onNestedPreFling</td> <td>在 Scrolling Child 开始处理 Fling 动作的时候,询问 Scrolling Parent 是否需要先处理 Fling 动作</td> </tr> <tr> <td>dispatchNestedFling</td> <td>onNestedFling</td> <td>在 Scrolling Child 处理 Fling 动作完毕的时候,询问 Scrolling Parent 是都还需要进行相应的处理</td> </tr> <tr> <td>stopNestedScroll</td> <td>onStopNestedScroll</td> <td>在 Scrolling Child 停止滑动的时候,会调用 Scrolling Parent 的这个方法。通常是在 Action_up 或者 Action_cancel 或者被别的 View 消费 Touch 事件的时候调用的</td> </tr> </tbody> </table> ### 执行流程 ### 1. 在 Action\_Down 的时候,Scrolling child 会调用 startNestedScroll 方法,通过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法 2. 在 Action\_move 的时候,Scrolling Child 要开始滑动的时候,会调用dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,若需要的话,会调用 Parent 的 onNestedPreScroll 方法,协同 Child 一起进行滑动 3. 当 ScrollingChild 滑动完成的时候,会调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动,需要的话,会 调用 Parent 的 onNestedScroll 方法 4. 在 Action\_down,Action\_move 的时候,会调用 Scrolling Child 的stopNestedScroll ,通过 ChildHelper 询问 Scrolling parent 的 stopNestedScroll 方法。 5. 如果需要处理 Fling 动作,我们可以通过 VelocityTrackerCompat 获得相应的速度,并在 Action\_up 的时候,调用 dispatchNestedPreFling 方法,通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作 6. 在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法 -------------------- 最后的最后,卖一下广告,欢迎大家关注我的微信公众号,扫一扫下方二维码或搜索微信号 stormjun,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。 ![9fe4afa0gy1fky6yqvcbbj209k09k748.jpg][] [CoordinatorLayout]: http://blog.csdn.net/gdutxiaoxu/article/details/52858598 [Behavior _ _FloatActionButton]: http://blog.csdn.net/gdutxiaoxu/article/details/53453958 [NestedScrolling]: http://blog.csdn.net/gdutxiaoxu/article/details/71553411 [_ CoordinatorLayout]: http://blog.csdn.net/gdutxiaoxu/article/details/71616547 [Behavior]: http://blog.csdn.net/gdutxiaoxu/article/details/71732642 [strip]: /images/20220616/6e9551589ad246f38448d8b8359f0527.png [9fe4afa0gy1ffg6uv3ucfj20lh0bjwfe.jpg]: https://ws1.sinaimg.cn/large/9fe4afa0gy1ffg6uv3ucfj20lh0bjwfe.jpg [9fe4afa0gy1fky6yqvcbbj209k09k748.jpg]: /images/20220616/5d16c8c9f1c84235b692a9920d84f956.png
相关 Java反射机制深入理解:通过实例解析 在Java编程中,反射机制是一个强大的工具,它允许我们在运行时检查类、方法和变量的信息。下面我们将通过一个具体的实例来深入理解反射机制。 首先,我们有一个简单的Java类: 我就是我/ 2024年09月18日 10:27/ 0 赞/ 3 阅读
相关 Java反射机制深入解析 Java的反射机制是一种强大的工具,它允许我们在运行时检查类、方法和字段的信息。以下是反射机制深入解析的关键点: 1. 类加载:当Java应用需要访问一个类时,Java虚拟机 亦凉/ 2024年09月15日 05:03/ 0 赞/ 7 阅读
相关 Java并发编程中锁机制问题:深入解析 在Java并发编程中,锁机制是一种同步工具,用于控制多个线程对共享资源的访问。以下是对锁机制的深度解析: 1. **synchronized**关键字: - `sync Love The Way You Lie/ 2024年09月12日 17:21/ 0 赞/ 18 阅读
相关 Java反射机制深入解析 Java的反射机制是一种强大的工具,它允许程序在运行时检查和修改类、对象及方法的信息。下面对Java反射机制进行深入解析: 1. **Class**:这是反射的主要对象类型, 忘是亡心i/ 2024年09月11日 19:57/ 0 赞/ 44 阅读
相关 Lambda表达式深入解析 1.Lambda介绍 lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body可以 超、凢脫俗/ 2022年12月22日 06:19/ 0 赞/ 190 阅读
相关 Spring 事件机制 - 源码深入解析 本篇文追踪源码详情,吐血输出,建议自己亲自动手追踪 Spring事件机制体验 要理解spring事件我们先来说说事件的几个角色概念 事件(Application 雨点打透心脏的1/2处/ 2022年09月04日 03:54/ 0 赞/ 196 阅读
相关 NestedScrolling 机制深入解析 [使用CoordinatorLayout打造各种炫酷的效果][CoordinatorLayout] [自定义Behavior —— 仿知乎,FloatActionButton 雨点打透心脏的1/2处/ 2022年06月16日 08:19/ 0 赞/ 217 阅读
相关 Android 嵌套滑动——NestedScrolling完全解析 基本的事件分发流程 对于一次从父布局到自布局的触摸事件流程分发,关键便是在三个方法上的流程处理`dispatchTouchEvent()`,`onInterceptTou 一时失言乱红尘/ 2022年06月06日 00:46/ 0 赞/ 403 阅读
相关 ReentrantReadWriteLock深入解析 ReentrantLock在并发情况下只允许单个线程执行受保护的代码,而在大部分应用中都是读多写少,所以,如果使用ReentrantLock实现这种对共享数据的并发访问控制,将 你的名字/ 2021年11月08日 20:50/ 0 赞/ 372 阅读
还没有评论,来说两句吧...