本文分析下上篇文章的布局情況。CoordinatorLayout的布局跟普通viewgroup不太一樣,behavior會(huì)插一手。本篇主要介紹behavior如何影響measure和layout。
提出問題
上文activity的xml如下
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.fish.behaviordemo.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_fab" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"
app:layout_behavior="com.fish.behaviordemo.fab.MyBehavior" />
</android.support.design.widget.CoordinatorLayout>
界面顯示效果如下所示
最外層是CoordinatorLayout,里面放了個(gè)AppBarLayout、content_fab、FloatingActionButton。我們先不管FloatingActionButton。都說CoordinatorLayout是個(gè)super FrameLayout,那這里的布局應(yīng)該是content_fab疊在AppBarLayout上咯?可是我們看到的是content_fab在AppBarLayout下方,這可不像FrameLayout,難道是被蓋住了一部分沒看到嗎?錯(cuò)了,content_fab的的確卻是在AppBarLayout的下方,這是CoordinatorLayout布局的時(shí)候定下來的。
我們這里的style如下,這個(gè)style的view tree內(nèi)是沒有statusbar的,可參考http://blog.csdn.net/litefish/article/details/52034813
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
那就有另一個(gè)問題了,為何AppBarLayout不在屏幕的頂上,而在statubar的下方。
帶著這2個(gè)問題,我們來看CoordinatorLayout的布局過程。
問題1:為何AppBarLayout會(huì)在statusbar的下方
問題2:為何content_fab在AppBarLayout的下方
列舉behavior
behavior是會(huì)介入onMeasure和onLayout過程的,我們先把各個(gè)子view的behavior找出來
AppBarLayout的behavior是由注解決定的
而content_fab的behavior是什么?content_fab是個(gè)RelativeLayout,后文我們稱RelativeLayout。
xml內(nèi)有這么一句話
app:layout_behavior="@string/appbar_scrolling_view_behavior"
@string/appbar_scrolling_view_behavior是什么?
<string name="appbar_scrolling_view_behavior" translatable="false">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>
其實(shí)從下面代碼可以看出是AppBarLayout.ScrollingViewBehavior
所以此處的CoordinatorLayout的3個(gè)子view和behavior如下所示
view | behavior |
---|---|
AppBarLayout | AppBarLayout.Behavior |
RelativeLayout | AppBarLayout.ScrollingViewBehavior |
FloatingActionButton | MyBehavior |
我們不管FloatingActionButton,看AppBarLayout和RelativeLayout,都挺復(fù)雜的,下圖是累關(guān)系圖,可以看到都是從ViewOffsetBehavior派生而來的。
再來看AppBarLayout.ScrollingViewBehavior內(nèi)的代碼,可以看到依賴于AppBarLayout,所以這里RelativeLayout依賴于AppBarLayout
//AppBarLayout.ScrollingViewBehavior
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
onMeasure分析
CoordinatorLayout的onMeasure方法如下所示,比較簡(jiǎn)單,主要流程是,先算出insets的值,然后measure的時(shí)候去掉這些insets,在measure 子view的時(shí)候,behavior先measure,返回false的話,才輪到view本身measure。
我們知道CoordinatorLayout是userRoot的根節(jié)點(diǎn),所以第一次measure CoordinatorLayout的heightMeasureSpec.size是第一次用1668(1794-126),第二次用1794(1920-126)。上邊這些數(shù)字都是在我手機(jī)上的值,1794是DisplayMetrics的高度值,126是navigatorbar的高度,1920是屏幕高度。
所以下邊代碼里,onMeasure參數(shù)heightMeasureSpec的size第一次是1668,第二次是1794。
下面開始具體分析
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
。。。
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
final int widthPadding = paddingLeft + paddingRight;
final int heightPadding = paddingTop + paddingBottom;
...
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
int childWidthMeasureSpec = widthMeasureSpec;
int childHeightMeasureSpec = heightMeasureSpec;
if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
// We're set to handle insets but this child isn't, so we will measure the
// child as if there are no insets
final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
+ mLastInsets.getSystemWindowInsetRight();
//獲取vertInsets值,其實(shí)這里就是statubar的高度
final int vertInsets = mLastInsets.getSystemWindowInsetTop()
+ mLastInsets.getSystemWindowInsetBottom();
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
widthSize - horizInsets, widthMode);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
heightSize - vertInsets, heightMode);
}
final Behavior b = lp.getBehavior();
//behavior先measure,返回false的話,才輪到view本身measure
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
}
。。。
}
final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec,
childState & ViewCompat.MEASURED_STATE_MASK);
final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec,
childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
setMeasuredDimension(width, height);
}
第一次measure,heightMeasureSpec.size為1668
prepareChildren和ensurePreDrawListener,之前在 這里分析過,不清楚的可以回顧下。
因?yàn)樽觱iew沒有寫fitsSystemWindows,所以L20會(huì)進(jìn)去,在L23會(huì)算出vertInsets,這里實(shí)際就是statubar的高度(63)。然后L29,修改childHeightMeasureSpec.size為1668-63=1605。從這里可以看出在measure的過程中,其實(shí)是除掉了statubar的高度,然后走到L35,先交給behavior measure。這里我們主要看下RelativeLayout這個(gè)child是如何measure的,RelativeLayout的behavior是AppBarLayout.ScrollingViewBehavior,他沒有復(fù)寫onMeasureChild方法,所以看父類HeaderScrollingViewBehavior。先看L7,只有MATCH_PARENT或WRAP_CONTENT,我們behavior才處理,否則直接返回false,丟給view自己處理,我們這里是MATCH_PARENT,由behavior處理。 然后看L13,找到一個(gè)header,這是個(gè)view,這個(gè)非常重要,是當(dāng)前view的第一個(gè)依賴view(可以稱為header),當(dāng)前view的各種操作都會(huì)依賴于header,明顯,我們的RelativeLayout的header就是AppBarLayout,再看L28,如果head沒有l(wèi)ayout過,那直接返回false,意思就是必須在head 布局完成之后,再來measure我們RelativeLayout。這個(gè)行為是比較奇怪的,一個(gè)view的measure居然依賴另一個(gè)view的layout。此時(shí),我們肯定沒有l(wèi)ayout過,所以直接返回false,然后走上邊代碼的L39,之后RelativeLayout的measuredHeight變?yōu)?605.
第二次measure,先看上文代碼,heightMeasureSpec.size為1794,在L28改為1794-63=1731,進(jìn)入下邊代碼,此時(shí)header還沒layout,所以返回false,依然是view自身measure,之后RelativeLayout的measuredHeight變?yōu)?794.
上2次measure都在layout之前,是通過view本身來measure的,我們?cè)诳纯磍ayout之后的measure
第三次measure(這次measure是怎么觸發(fā)的呢?),此時(shí)已經(jīng)layout過了
先看上文代碼,heightMeasureSpec.size為1794,在L29改為1794-63=1731,進(jìn)入下邊代碼,此時(shí)header已經(jīng)layout,所以進(jìn)入L28的if內(nèi),重點(diǎn)關(guān)注L35,
final int height = availableHeight - header.getMeasuredHeight()
+ getScrollRange(header);
翻一下就是parent的高度-header的measuredHeight+header的滾動(dòng)范圍
這里減掉了一個(gè)header.getMeasuredHeight(),加上了getScrollRange(header),后者我們暫時(shí)不考慮,此時(shí)其實(shí)就是減去了AppBarlayout的measuredHeight,在這里就是1731-147=1584,然后調(diào)用 CoordinatorLayout.onMeasureChild,最后measure結(jié)果就是1584,下邊代碼返回true。這其實(shí)是HeaderScrollingViewBehavior的一個(gè)特性,讓當(dāng)前view處于header的下方。
好了,三輪measure下來,最終RelativeLayout的measuredHeight被定為1584
//HeaderScrollingViewBehavior
@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child,
int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
|| childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
// If the menu's height is set to match_parent/wrap_content then measure it
// with the maximum visible height
final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
if (ViewCompat.getFitsSystemWindows(header)
&& !ViewCompat.getFitsSystemWindows(child)) {
// If the header is fitting system windows then we need to also,
// otherwise we'll get CoL's compatible measuring
ViewCompat.setFitsSystemWindows(child, true);
if (ViewCompat.getFitsSystemWindows(child)) {
// If the set succeeded, trigger a new layout and return true
child.requestLayout();
return true;
}
}
//header未layout,我就不measure
if (ViewCompat.isLaidOut(header)) {
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight == 0) {
// If the measure spec doesn't specify a size, use the current height
availableHeight = parent.getHeight();
}
final int height = availableHeight - header.getMeasuredHeight()
+ getScrollRange(header);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
? View.MeasureSpec.EXACTLY
: View.MeasureSpec.AT_MOST);
// Now measure the scrolling view with the correct height
parent.onMeasureChild(child, parentWidthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
}
return false;
}
onLayout分析
看下邊CoordinatorLayout的onLayout,發(fā)現(xiàn)布局的子view的時(shí)候,先由behavior處理,behavior未處理成功再交給child處理,跟onMeasure類似。
@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);
}
}
}
我們?cè)賮矸治?,因?yàn)镽elativeLayout依賴于AppBarLayout,所以在mDependencySortedChildren內(nèi),AppBarLayout在前,RelativeLayout在后。
布局AppBarLayout
先看如何布局AppBarLayout。
step1
看AppBarLayout.Behavior的onLayoutChild,首先調(diào)用了super.onLayoutChild,會(huì)調(diào)用到ViewOffsetBehavior的onLayoutChild
//AppBarLayout.Behavior
@Override
public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl,
int layoutDirection) {
boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
...
}
step2
再看ViewOffsetBehavior的onLayoutChild,調(diào)用layoutChild,然后調(diào)用parent.onLayoutChild,parent是誰,CoordinatorLayout
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
// First let lay the child out
layoutChild(parent, child, layoutDirection);
if (mViewOffsetHelper == null) {
mViewOffsetHelper = new ViewOffsetHelper(child);
}
mViewOffsetHelper.onViewLayout();
if (mTempTopBottomOffset != 0) {
mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);
mTempTopBottomOffset = 0;
}
if (mTempLeftRightOffset != 0) {
mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);
mTempLeftRightOffset = 0;
}
return true;
}
protected void layoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
// Let the parent lay it out by default
parent.onLayoutChild(child, layoutDirection);
}
step3
CoordinatorLayout的onLayoutChild一般會(huì)調(diào)用layoutChild,看完這段代碼,就應(yīng)該能明白問題1。里面有個(gè)parent叫Rect,這個(gè)Rect代表CoordinatorLayout內(nèi)部可以放子view的空間,一開始的時(shí)候parent的top為0,在L15把statusbar的高度加到top里去,這其實(shí)就是為了讓AppbarLayout不要和statusbar重疊。在L21根據(jù)AppbarLayout的getMeasuredHeight()和parent,算出一個(gè)Rect out,用這個(gè)Rect來給AppbarLayout布局,out里的top必定是statusbar的高度,在L23 child.layout內(nèi)AppbarLayout的mTop必然被設(shè)置為statusbar的高度,所以問題1解決,核心代碼就是layoutChild。注意layoutChild不是針對(duì)AppbarLayout,所以任何子view都不可能跑到statubar上。
private void layoutChild(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect parent = mTempRect1;
parent.set(getPaddingLeft() + lp.leftMargin,
getPaddingTop() + lp.topMargin,
getWidth() - getPaddingRight() - lp.rightMargin,
getHeight() - getPaddingBottom() - lp.bottomMargin);
if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
&& !ViewCompat.getFitsSystemWindows(child)) {
// If we're set to handle insets but this child isn't, then it has been measured as
// if there are no insets. We need to lay it out to match.
parent.left += mLastInsets.getSystemWindowInsetLeft();
//這里把statusbar的高度算進(jìn)去了
parent.top += mLastInsets.getSystemWindowInsetTop();
parent.right -= mLastInsets.getSystemWindowInsetRight();
parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
}
final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
child.getMeasuredHeight(), parent, out, layoutDirection);
child.layout(out.left, out.top, out.right, out.bottom);
}
step4 mViewOffsetHelper.onViewLayout
此時(shí),其實(shí)相當(dāng)于給AppBarLayout設(shè)置了一個(gè)額外的padding,這個(gè)值會(huì)被記錄下來,在onLayoutChild的L8,有mViewOffsetHelper.onViewLayout.由下邊代碼可知mLayoutTop就記錄了這個(gè)額外的padding。ViewOffsetHelper內(nèi)部有mLayoutTop,mOffsetTop。mLayoutTop代表基本top,mOffsetTop代表額外top偏移量,實(shí)際view的top為 mLayoutTop+ mOffsetTop。ViewOffsetHelper內(nèi)的setTopAndBottomOffset的參數(shù)offset是一個(gè)絕對(duì)值,但是view的offsetTopAndBottom的參數(shù)offset是一個(gè)delta值,mLayoutTop就可以把絕對(duì)值轉(zhuǎn)化為delta值。
public void onViewLayout() {
// Now grab the intended top
mLayoutTop = mView.getTop();
mLayoutLeft = mView.getLeft();
// And offset it as needed
updateOffsets();
}
布局RelativeLayout
RelativeLayout的behavior是AppBarLayout.ScrollingViewBehavior
step1
和布局AppBarLayout一樣,會(huì)調(diào)用ViewOffsetBehavior的onLayoutChild
step2
ViewOffsetBehavior的onLayoutChild,調(diào)用layoutChild,此時(shí)layoutChild可不一樣了,因?yàn)镠eaderScrollingViewBehavior復(fù)寫了??聪逻叴a可以解決我們的第二個(gè)問題,首先尋找依賴的view,我們的RelativeLayout依賴AppBarLayout,然后看L13,這個(gè)代碼非常關(guān)鍵,available這個(gè)Rect設(shè)置在AppBarLayout的下方,然后類似的在L20啊,根據(jù)getMeasuredHeight()和available計(jì)算出out,再用out來layout RelativeLayout。所以RelativeLayout必然在AppBarLayout的下方,這是由它的behavior決定的。HeaderScrollingViewBehavior要求排在首個(gè)依賴view(header)的下方
@Override
protected void layoutChild(final CoordinatorLayout parent, final View child,
final int layoutDirection) {
final List<View> dependencies = parent.getDependencies(child);
//尋找依賴
final View header = findFirstDependency(dependencies);
if (header != null) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
final Rect available = mTempRect1;
//這個(gè)rect在依賴view的下邊
available.set(parent.getPaddingLeft() + lp.leftMargin,
header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom()
- parent.getPaddingBottom() - lp.bottomMargin);
final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
child.getMeasuredHeight(), available, out, layoutDirection);
final int overlap = getOverlapPixelsForOffset(header);
child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
mVerticalLayoutGap = out.top - header.getBottom();
} else {
// If we don't have a dependency, let super handle it
super.layoutChild(parent, child, layoutDirection);
mVerticalLayoutGap = 0;
}
}
總結(jié)
1、CoordinatorLayout像一個(gè)FrameLayout,但是里面的布局受behavior影響,我們可以通過改寫behavior來修改布局策略
2、CoordinatorLayout的高度包括statubar,如果CoordinatorLayout的子view沒有寫fitsSystemWindows,都不可能跑到statubar上,因?yàn)閙easure的時(shí)候會(huì)除掉statubar,layoutChild的時(shí)候也會(huì)處理。
3、如果CoordinatorLayout的子view重寫了fitsSystemWindows,那么子view的范圍會(huì)包括statubar
4、HeaderScrollingViewBehavior有個(gè)特性,使用此behavior的view 必然排在他的首個(gè)依賴view(簡(jiǎn)稱header)的下方,因?yàn)閺?fù)寫了layoutChild