返回介绍

4. fab 与 CoordinatorLayout 的交互

发布于 2024-12-23 22:29:23 字数 12849 浏览 0 评论 0 收藏 0

这块内容因为与 CoordinatorLayout / CoordinatorLayout#Behavior 有很大关联,如果不熟悉,请先 google 相关资料。本文假设读者对这块内容已经有一定理解。

fab 并不直接与 CoordinatorLayout 联系,而是通过 CoordinatorLayout#Behavior 作为桥梁。 CoordinatorLayout 类通过 CoordinatorLayout#Behavior 可以间接控制其直系子 View 的行为,能控制什么行为?View 测量、布局、touch 事件拦截、监听、NestedScroll 等等。是不是很屌。

fab 内部实现了 CoordinatorLayout#Behavior 抽象类。该抽象类有如下接口:

public static abstract class Behavior<V extends View> {

		...
   
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
      return false;
    }

    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
      return false;
    }

     ...
    /**
     * Determine whether the supplied child view has another specific sibling view as a
     * layout dependency.
     *
     * <p>This method will be called at least once in response to a layout request. If it
     * returns true for a given child and dependency view pair, the parent CoordinatorLayout
     * will:</p>
     * <ol>
     *   <li>Always lay out this child after the dependent child is laid out, regardless
     *   of child order.</li>
     *   <li>Call {@link #onDependentViewChanged} when the dependency view's layout or
     *   position changes.</li>
     * </ol>
     */
    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
      return false;
    }

    /**
     * Respond to a change in a child's dependent view
     *
     * <p>This method is called whenever a dependent view changes in size or position outside
     * of the standard layout flow. A Behavior may use this method to appropriately update
     * the child view in response.</p>
     *
     * <p>A view's dependency is determined by
     * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
     * if {@code child} has set another view as it's anchor.</p>
     *
     * <p>Note that if a Behavior changes the layout of a child via this method, it should
     * also be able to reconstruct the correct position in
     * {@link #onLayoutChild(CoordinatorLayout, android.view.View, int) onLayoutChild}.
     * <code>onDependentViewChanged</code> will not be called during normal layout since
     * the layout of each child view will always happen in dependency order.</p>
     *
     * <p>If the Behavior changes the child view's size or position, it should return true.
     * The default implementation returns false.</p>
     *
     */
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
      return false;
    }

			...

  

    /**
     * Called when the parent CoordinatorLayout is about the lay out the given child view.
     *
     * <p>This method can be used to perform custom or modified layout of a child view
     * in place of the default child layout behavior. The Behavior's implementation can
     * delegate to the standard CoordinatorLayout measurement behavior by calling
     * {@link CoordinatorLayout#onLayoutChild(android.view.View, int)
     * parent.onLayoutChild}.</p>
     *
     * <p>If a Behavior implements
     * {@link #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)}
     * to change the position of a view in response to a dependent view changing, it
     * should also implement <code>onLayoutChild</code> in such a way that respects those
     * dependent views. <code>onLayoutChild</code> will always be called for a dependent view
     * <em>after</em> its dependency has been laid out.</p>
     *
     */
    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
      return false;
    }
 
    ...
   
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
      // Do nothing
    }

  
  }


看到这个抽象类,有两点需要注意:

  1. 此抽象类并无抽象方法,也即子类可选择任何想复写的方法进行复写。
  2. 此抽象类接受一个泛型。该泛型需要是 View 的子类。

fab 实现此抽象类:

public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {}

有选择性地实现了三个方法:

public boolean layoutDependsOn(CoordinatorLayout parent,
        FloatingActionButton child, View dependency);
        
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
        View dependency);

 public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
        int layoutDirection);      
                

fab 为啥要实现 Behavior ?主要是为了配合其他控件完成一些复杂的交互,比较经典的像这个: fab 动画效果

fab 需要在 snackBar 弹出的时候自动向上平移,这就得知道 SnackBar 的状态了,实现 Behavior 让 fab 有机会监听到其他 CoordinatorLayout 子 View 的状态,并根据状态更新自己。

复写 layoutDependsOn 方法可以告诉 CoordinatorLayout 我对哪个 View 感兴趣,

这里当然是 SnackBar 了。(注意哦,SnackBar 最终展现的是 SnackbarLayout,SnackBar 本身并不是 View)

private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;

 @Override
    public boolean layoutDependsOn(CoordinatorLayout parent,
        FloatingActionButton child, View dependency) {
      // We're dependent on all SnackbarLayouts (if enabled)
      return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
    }

为什么 API LEVEL 要大于 11 呢?因为 google 偷懒想直接使用属性动画。

前面告诉了 CoordinatorLayout fab 对 SnackBar 比较感兴趣,那么当 SnackBar 状态改变的时候, CoordinatorLayout 就会通过 onDependentViewChanged 回调通知 fab:

fab 就可以更新自己的 UI 拉(这里当然是平移喽):

@Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
        View dependency) {
      if (dependency instanceof Snackbar.SnackbarLayout) {
        updateFabTranslationForSnackbar(parent, child, dependency);
      } else if (dependency instanceof AppBarLayout) {
        // If we're depending on an AppBarLayout we will show/hide it automatically
        // if the FAB is anchored to the AppBarLayout
        updateFabVisibility(parent, (AppBarLayout) dependency, child);
      }
      return false;
    }

如果是 SnackBar 状态变化了,那么 fab 就会根据情况进行平移:

private void updateFabTranslationForSnackbar(CoordinatorLayout parent,
        final FloatingActionButton fab, View snackbar) {
      final float targetTransY = getFabTranslationYForSnackbar(parent, fab);
      if (mFabTranslationY == targetTransY) {
        // We're already at (or currently animating to) the target value, return...
        return;
      }

      final float currentTransY = ViewCompat.getTranslationY(fab);

      // Make sure that any current animation is cancelled
      if (mFabTranslationYAnimator != null && mFabTranslationYAnimator.isRunning()) {
        mFabTranslationYAnimator.cancel();
      }

      if (fab.isShown()
          && Math.abs(currentTransY - targetTransY) > (fab.getHeight() * 0.667f)) {
        // If the FAB will be travelling by more than 2/3 of it's height, let's animate
        // it instead
        if (mFabTranslationYAnimator == null) {
          mFabTranslationYAnimator = ViewUtils.createAnimator();
          mFabTranslationYAnimator.setInterpolator(
              AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
          mFabTranslationYAnimator.setUpdateListener(
              new ValueAnimatorCompat.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimatorCompat animator) {
                  ViewCompat.setTranslationY(fab,
                      animator.getAnimatedFloatValue());
                }
              });
        }
        mFabTranslationYAnimator.setFloatValues(currentTransY, targetTransY);
        mFabTranslationYAnimator.start();
      } else {
        // Now update the translation Y
        ViewCompat.setTranslationY(fab, targetTransY);
      }

      mFabTranslationY = targetTransY;
    }

代码里的注释很多,我就不解释了。

前面说到 AppBarLayout 和 fab 一起使用可以完成另一个效果,即 AppBarLayout 伸缩时,fab 也可以以动画的形式显现、隐藏,其实现如下:

private boolean updateFabVisibility(CoordinatorLayout parent,
        AppBarLayout appBarLayout, FloatingActionButton child) {
      final CoordinatorLayout.LayoutParams lp =
          (CoordinatorLayout.LayoutParams) child.getLayoutParams();
      //注意到我们必须为 fab 指定 layout_anchor 为 appBarLayout          
      if (lp.getAnchorId() != appBarLayout.getId()) {
        // The anchor ID doesn't match the dependency, so we won't automatically
        // show/hide the FAB
        return false;
      }

      if (child.getUserSetVisibility() != VISIBLE) {
        // The view isn't set to be visible so skip changing it's visibility
        return false;
      }

      if (mTmpRect == null) {
        mTmpRect = new Rect();
      }

      // First, let's get the visible rect of the dependency
      final Rect rect = mTmpRect;
      ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);

      if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
        // If the anchor's bottom is below the seam, we'll animate our FAB out
        child.hide(null, false);
      } else {
        // Else, we'll animate our FAB back in
        child.show(null, false);
      }
      return true;
    }

除此之外, fab#Behavior 还实现了 onLayoutChild ,主要是为了根据 AppBarLayout 的当前状态来判断自己是否需要隐藏。

 @Override
    public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
        int layoutDirection) {
      // First, lets make sure that the visibility of the FAB is consistent
      final List<View> dependencies = parent.getDependencies(child);
      for (int i = 0, count = dependencies.size(); i < count; i++) {
        final View dependency = dependencies.get(i);
        if (dependency instanceof AppBarLayout
            && updateFabVisibility(parent, (AppBarLayout) dependency, child)) {
          break;
        }
      }
      // Now let the CoordinatorLayout lay out the FAB
      parent.onLayoutChild(child, layoutDirection);
      // Now offset it if needed
      offsetIfNeeded(parent, child);
      return true;
    }

此方法会在 CoordinatorLayout 对孩子布局的时候进行调用(即 CoordinatorLayout#onLayout ), CoordinatorLayout 会检查所有的直系孩子,是否设置了 Behavior,如果设置了,那么就执行其 onLayoutChild 方法:

CoordinatorLayout#onLayout

 @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
      final View child = mDependencySortedChildren.get(i);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      final Behavior behavior = lp.getBehavior();

      if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
        onLayoutChild(child, layoutDirection);
      }
    }
  }

如果该 Behavior 实现了 OnLayoutChild,并且返回了 true,那么将不会执行 CoordinatorLayout #onLayoutChild ,否则执行默认的布局方案。 最后一点,这里的 Behavior 如何生效的呢?通过注解:

@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends VisibilityAwareImageButton {

CoordinatorLayout 在解析孩子的 LayoutParams 时,会 check 有无注解:

  LayoutParams getResolvedLayoutParams(View child) {
    final LayoutParams result = (LayoutParams) child.getLayoutParams();
    if (!result.mBehaviorResolved) {
      Class<?> childClass = child.getClass();
      DefaultBehavior defaultBehavior = null;
      while (childClass != null &&
          (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
        childClass = childClass.getSuperclass();
      }
      if (defaultBehavior != null) {
        try {
          result.setBehavior(defaultBehavior.value().newInstance());
        } catch (Exception e) {
          Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
              " could not be instantiated. Did you forget a default constructor?", e);
        }
      }
      result.mBehaviorResolved = true;
    }
    return result;
  }

至此 fab 解析完毕,谢谢观看!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

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