- CompoundButton 源码分析
- LinearLayout 源码分析
- SearchView 源码解析
- LruCache 源码解析
- ViewDragHelper 源码解析
- BottomSheets 源码解析
- Media Player 源码分析
- NavigationView 源码解析
- Service 源码解析
- Binder 源码分析
- Android 应用 Preference 相关及源码浅析 SharePreferences 篇
- ScrollView 源码解析
- Handler 源码解析
- NestedScrollView 源码解析
- SQLiteOpenHelper/SQLiteDatabase/Cursor 源码解析
- Bundle 源码解析
- LocalBroadcastManager 源码解析
- Toast 源码解析
- TextInputLayout
- LayoutInflater 和 LayoutInflaterCompat 源码解析
- TextView 源码解析
- NestedScrolling 事件机制源码解析
- ViewGroup 源码解析
- StaticLayout 源码分析
- AtomicFile 源码解析
- AtomicFile 源码解析
- Spannable 源码分析
- Notification 之 Android 5.0 实现原理
- CoordinatorLayout 源码分析
- Scroller 源码解析
- SwipeRefreshLayout 源码分析
- FloatingActionButton 源码解析
- AsyncTask 源码分析
- TabLayout 源码解析
4. NestedScrollView 之 Nested
还记得前面我们跟到了 mChildHelper.startNestedScroll 函数么,那个函数的主要工作就是要找到一个支持 nested 功能的 mNestedScrollingParent。哦,其实应该是 ancestorView。明眼人掐指一算,这个支持 nested 功能的 view 不就是我们熟悉的 CoordinatorLayout 么?此处,我们先建立一个大前提,layout 文件中我们让 NestedScrollView 支持视差,使用 CoordinatorLayout 和 AppbarLayout,xml 如下:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/main_content" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:elevation="6dp" app:layout_scrollFlags="scroll|enterAlways|snap" app:navigationIcon="?attr/homeAsUpIndicator" /> <Button android:layout_width="fill_parent" android:layout_height="60dp" android:background="#111111" android:gravity="center" android:textColor="#ffffff" android:text="这个是悬停按钮" /> </android.support.design.widget.AppBarLayout> <nested.stone.com.nestedscrolldemo.CoordinatorNestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <!--xxxxxxxxxxxxxxx 若干子 Viewxxxxxxxxxxxxxxxx--> </LinearLayout> </nested.stone.com.nestedscrolldemo.CoordinatorNestedScrollView> </android.support.design.widget.CoordinatorLayout>
接下来我们回归 java 代码,mChildHelper.startNestedScroll 可以找到支持 nested 功能的 parentView,那一段代码是这样的:
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; }
其中 ViewParentCompat.onStartNestedScroll 就是判断 mView 的某一个祖先视图 p 是否支持 nested 操作的。我们继续跟进去 ViewParentCompat 看看:
static final ViewParentCompatImpl IMPL; static { final int version = Build.VERSION.SDK_INT; if (version >= 21) { IMPL = new ViewParentCompatLollipopImpl(); } else if (version >= 19) { IMPL = new ViewParentCompatKitKatImpl(); } else if (version >= 14) { IMPL = new ViewParentCompatICSImpl(); } else { IMPL = new ViewParentCompatStubImpl(); } } public static boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes) { return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes); }
由于操作系统版本的不同,IMPL 有不同的实现类。我们看一下支持安卓 5.0 以后的这个兼容类:ViewParentCompatLollipopImpl
public static boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes) { try { return parent.onStartNestedScroll(child, target, nestedScrollAxes); } catch (AbstractMethodError e) { Log.e(TAG, "ViewParent " + parent + " does not implement interface " + "method onStartNestedScroll", e); return false; } }
在上面的函数中,parent 是 NestedScrollView 的某一个祖先视图。由于我们在 xml 中定义了 CoordinatorLayout,所以这个 parent 就应该是 CoordinatorLayout。然后呢,调用了这个 parent.onStartNestedScroll 函数,我们看一下 CoordinatorLayout 有没有这个函数。
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { boolean handled = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { // 遍历每一个子 View final View view = getChildAt(i); final LayoutParams lp = (LayoutParams) view.getLayoutParams(); final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target, nestedScrollAxes); handled |= accepted; lp.acceptNestedScroll(accepted); } else { lp.acceptNestedScroll(false); } } return handled; }
你看到了么,CoordinatorLayout 有这个函数。在这个函数中,它遍历了 CoordinatorLayout 的所有子 view,并找到了子 View 中对应的 LayoutParams.Behavior。所以,AppbarLayout 的 Behavior 就能排上用场了。它接下来会跳转到 AppbarLayout.Behavior.onStartNestedScroll 了。接下来我们就不跟了吧,这个只是 nested 功能开始的一个回调了。
接下来我们关心视差吧,回到 NestedScrollView.onTouchEvent 里面的 ACTION_MOVE 代码段:
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { mLastMotionY -= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; }
之所以挑出来这一段,是因为 dispatchNestedScroll 比较眼熟。我们跟进去 dispatchNestedScroll 瞅瞅:
@Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); }
又是 mChildHelper,我之前说过了它很强大,现在信了吧。此处我们跟进去这个 mChildHelper.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]; } 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; }
在这个函数中,支持 nested 功能的 parentView 如果愿意消费 MotionEvent,就返回 true,如果不愿意就返回 false。此外还有两个参数 dyUnconsumed 和 offsetInWindow,既是入参也是出参,意为子 View 愿意消费多少 dy。ViewParentCompat.onNestedScroll 就是真正把愿意消费的视差信息传递给 CoordinatorLayout 和 Behavior 了。后续的逻辑跟踪,你应该也大致看的懂了。
读到这份源代码,我一直忽略了坐标值细节的一针一眼的计算。因为比较费时间,费精力。有兴趣的朋友私下去感受就好。
当然,滑动时候,跟 CoordinatorLayout 和 Behavior 打交道还有其它一些函数,比如 stopNestedScroll 和 dispatchNestedFling 等。主要的一个逻辑,都是类似的,就是通过 mChildHelper 找到 mNestedScrollingParent,然后再由 mNestedScrollingParent 找到对应子 View 的 Behavior。限于篇幅问题,此处不再赘述了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论