9anchor

来源:互联网 发布:钢结构公司起名 知乎 编辑:程序博客网 时间:2024/05/18 10:57

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 0