9anchor

CoordinatorLayout还提供了一种布局方式叫anchor,看下边效果

对应xml

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        app:layout_anchor="@id/appbar"
        app:layout_anchorGravity="bottom|right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_dialog_email"
        app:layout_behavior="com.fish.behaviordemo.fab.MyBehavior" />

这里用了anchor把fab anchor到appbar上,anchor在appbar哪个位置呢?看layout_anchorGravity,这里是放在右下方。

布局

此时有2个问题,为什么fab会有一半在appbar内?写了layout_margin为什么只有右边生效了。

为什么一开始fab有一半在appbar内部,一般在appbar外部呢?因为fab没写layout_gravity,所以垂直方向是居中的,居中就会有一半在appbar内部(原因后边会讲),如果写个 android:layout_gravity=”bottom”,那就可以使得fab在appbar下方了,如下所示。

CoordinatorLayout.LayoutParams内部有4个成员是和anchor相关的,anchorGravity,mAnchorId,mAnchorView,mAnchorDirectChild。

mAnchorId是由app:layout_anchor指定的anchor对象的id。
mAnchorView是anchor的view,跟mAnchorId对应的,不一定是CoordinatorLayout的直接子view。
mAnchorDirectChild是CoordinatorLayout的直接子view,是mAnchorView本身或者祖先。
anchorGravity是anchor的方式,比如本文中就是bottom|right

//CoordinatorLayout.LayoutParams
    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        。。。
        /**
         * A {@link Gravity} value describing which edge of a child view's
         * {@link #getAnchorId() anchor} view the child should position itself relative to.
         */
        public int anchorGravity = Gravity.NO_GRAVITY;

        int mAnchorId = View.NO_ID;

        View mAnchorView;
        View mAnchorDirectChild;
        }

app:layout_anchor=”@id/appbar”这句话会导致fab的LayoutParams内有mAnchorView指向appbar。再看CoordinatorLayout的布局代码,可以看到mAnchorView非空会调用layoutChildWithAnchor

//CoordinatorLayout
    public void onLayoutChild(View child, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp.checkAnchorChanged()) {
            throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
                    + " measurement begins before layout is complete.");
        }
        if (lp.mAnchorView != null) {
            layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
        } else if (lp.keyline >= 0) {
            layoutChildWithKeyline(child, lp.keyline, layoutDirection);
        } else {
            layoutChild(child, layoutDirection);
        }
    }

再看layoutChildWithAnchor

//CoordinatorLayout
    private void layoutChildWithAnchor(View child, View anchor, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        final Rect anchorRect = mTempRect1;
        final Rect childRect = mTempRect2;
        getDescendantRect(anchor, anchorRect);
        getDesiredAnchoredChildRect(child, layoutDirection, anchorRect, childRect);

        child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
    }

而layoutChildWithAnchor内蛀要看getDesiredAnchoredChildRect。这里是根据anchorView的位置以及view的layout_anchorGravity、layout_gravity来敲定当前view的位置,layout_gravity 没写的话,L73可以看到会往上移动半个身位的高度,问题1解决。再看L84-L88,可以理解xml内的android:layout_margin限制的是离CoordinatorLayout上下左右的间距,大于等于这个值就可以了。

    void getDesiredAnchoredChildRect(View child, int layoutDirection, Rect anchorRect, Rect out) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final int absGravity = GravityCompat.getAbsoluteGravity(
                resolveAnchoredChildGravity(lp.gravity), layoutDirection);
        final int absAnchorGravity = GravityCompat.getAbsoluteGravity(
                resolveGravity(lp.anchorGravity),
                layoutDirection);

        final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
        final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int anchorHgrav = absAnchorGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
        final int anchorVgrav = absAnchorGravity & Gravity.VERTICAL_GRAVITY_MASK;

        final int childWidth = child.getMeasuredWidth();
        final int childHeight = child.getMeasuredHeight();

        int left;
        int top;

        // Align to the anchor. This puts us in an assumed right/bottom child view gravity.
        // If this is not the case we will subtract out the appropriate portion of
        // the child size below.
        switch (anchorHgrav) {
            default:
            case Gravity.LEFT:
                left = anchorRect.left;
                break;
            case Gravity.RIGHT:
                left = anchorRect.right;
                break;
            case Gravity.CENTER_HORIZONTAL:
                left = anchorRect.left + anchorRect.width() / 2;
                break;
        }

        switch (anchorVgrav) {
            default:
            case Gravity.TOP:
                top = anchorRect.top;
                break;
            case Gravity.BOTTOM:
                top = anchorRect.bottom;
                break;
            case Gravity.CENTER_VERTICAL:
                top = anchorRect.top + anchorRect.height() / 2;
                break;
        }

        // Offset by the child view's gravity itself. The above assumed right/bottom gravity.
        switch (hgrav) {
            default:
            case Gravity.LEFT:
                left -= childWidth;
                break;
            case Gravity.RIGHT:
                // Do nothing, we're already in position.
                break;
            case Gravity.CENTER_HORIZONTAL:
                left -= childWidth / 2;
                break;
        }

        switch (vgrav) {
            default:
            case Gravity.TOP:
                top -= childHeight;
                break;
            case Gravity.BOTTOM:
                // Do nothing, we're already in position.
                break;
            case Gravity.CENTER_VERTICAL:
            //往上移一半高度
                top -= childHeight / 2;
                break;
        }
         //注意这是CoordinatorLayout的宽高
        final int width = getWidth();
        final int height = getHeight();

        // Obey margins and padding
        left = Math.max(getPaddingLeft() + lp.leftMargin,
                Math.min(left,
                        width - getPaddingRight() - childWidth - lp.rightMargin));
        top = Math.max(getPaddingTop() + lp.topMargin,
                Math.min(top,
                        height - getPaddingBottom() - childHeight - lp.bottomMargin));

        out.set(left, top, left + childWidth, top + childHeight);
    }

滑动

为什么fab会随着appbar的滑动而滑动呢?
anchor顾名思义是固定在某个view上的,这个view滑动,他也如影随形,这是怎么实现的?
首先当前view和anchorview直接建立依赖关系,依赖于anchorview. anchorview滑动的时候会触发dispatchOnDependentViewChanged,内部调用offsetChildToAnchor

//CoordinatorLayout
   void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
        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();

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                //key code
                    offsetChildToAnchor(child, layoutDirection);
                }
            }
            。。。
      }

关键代码offsetChildToAnchor,可以看到里面跟layoutChildWithAnchor的逻辑类似的,进行offsetLeftAndRight、offsetTopAndBottom来改变位置。

//CoordinatorLayout
   /**
     * Adjust the child left, top, right, bottom rect to the correct anchor view position,
     * respecting gravity and anchor gravity.
     *
     * Note that child translation properties are ignored in this process, allowing children
     * to be animated away from their anchor. However, if the anchor view is animated,
     * the child will be offset to match the anchor's translated position.
     */
    void offsetChildToAnchor(View child, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp.mAnchorView != null) {
            final Rect anchorRect = mTempRect1;
            final Rect childRect = mTempRect2;
            final Rect desiredChildRect = mTempRect3;

            getDescendantRect(lp.mAnchorView, anchorRect);
            getChildRect(child, false, childRect);
            getDesiredAnchoredChildRect(child, layoutDirection, anchorRect, desiredChildRect);

            final int dx = desiredChildRect.left - childRect.left;
            final int dy = desiredChildRect.top - childRect.top;

            if (dx != 0) {
                child.offsetLeftAndRight(dx);
            }
            if (dy != 0) {
                child.offsetTopAndBottom(dy);
            }

            if (dx != 0 || dy != 0) {
                // If we have needed to move, make sure to notify the child's Behavior
                final Behavior b = lp.getBehavior();
                if (b != null) {
                    b.onDependentViewChanged(this, child, lp.mAnchorView);
                }
            }
        }
    }

fab默认的

我们去掉这行代码,看看会发生什么
app:layout_behavior=”com.fish.behaviordemo.fab.MyBehavior”

滑到顶的时候fab消失了,这是怎么做到的呢?
此时的behavior由注解决定,是FloatingActionButton.Behavior。相关代码如下,在onDependentViewChanged发现dependency是AppBarLayout就会调用updateFabVisibility

//FloatingActionButton.Behavior
  @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;
        }

再看updateFabVisibility,

//FloatingActionButton.Behavior
        private boolean updateFabVisibility(CoordinatorLayout parent,
                AppBarLayout appBarLayout, FloatingActionButton child) {
            final CoordinatorLayout.LayoutParams lp =
                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            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;
        }

这里主要看L22,首先获取appBarLayout的可见区域rect,然后根据rect的bottom来判断是否够小了,够小了,就hide隐藏掉,否则就show显示。那隐藏的动画是怎么实现的呢?相关代码在design_fab_out.xml内部,如下所示,简单易懂。


<set xmlns:android="http://schemas.android.com/apk/res/android">

    <alpha android:fromAlpha="1.0"
           android:toAlpha="0.0"/>

    <scale android:fromXScale="1.0"
           android:fromYScale="1.0"
           android:toXScale="0.0"
           android:toYScale="0.0"
           android:pivotX="50%"
           android:pivotY="50%"/>

</set>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值