返回介绍

4. NestedScrollView 之 Nested

发布于 2024-12-23 21:38:51 字数 6861 浏览 0 评论 0 收藏 0

还记得前面我们跟到了 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文