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>