Android的Measure过程作为View的三部曲(measure,layout,draw)的第一步,承担了View到底宽高是多少这个问题。笔者之前也看过很多文章,但是对其中的关键原理并不完全知晓,大多数博文一上来就花费大量篇幅来描述MeasureSpec这个类,那么这个类到底是做什么呢?它的几种模式又分别是什么意思呢。我们就来看看View的measure过程中,Android系统是如何解决View的宽高问题的。
(本文不关注其他内容……什么onMeasure啥时候调用啊,measure
方法和onMeasure
的关系啊,为什么measure
方法是final
的啊,WindowManager
和WindowManager#LayoutParams
啊,ViewRootImpl
啊,都不管……)
在看接下来一些内容的时候,我推荐阅读一些其他精彩的博客。如果这两篇都不读的话……你估计很难知道我在说什么……
ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解
Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(下)
本文分为两个部分,第一部分简要概述了measure过程中一些关键的内容。第二部分是将TabLayout作为例子,看看具体怎么做的。
Part 1
概述
通常情况下,我们是在xml
文件或者LayoutParams
类里面,去指定一个View
的宽高,我们一般会写具体的值,或者MATCH_PARENT
和WRAP_CONTENT
,但是,需要注意的一点是,一个View
的最终宽高不只是它自己指定的宽高,也要它所属的ViewGroup
(就是视图层级上的父View)所决定的。比如说一个View的宽度指定为MATCH_PARENT
,如果你的父View是LinearLayout
,那么父View根据你这个MATCH_PARENT
的标记,判断你需要整个View的宽度,所以如果只有你这一个子View的情况下,宽度就直接等于父View的宽度。但是在其他情况下呢?如果是WRAP_CONTENT
呢?程序在处理的时候只能认为这是一个“匹配内容”的标记,而不是它需要的具体宽高,因为真正的逻辑是肯定需要一个具体的值来进行测量的,所以,MeasureSpec
类出现了。MeasureSpec
类通过一系列准则和方法,重新处理了来自xml
文件或者LayoutParams
中的size,使得View最终都有一个精确的宽高。
所以简单来说,我们通常写的宽高,对于Android系统来说只是一个参考值,内部需要一套新规则来重新定义View的宽高,这个规则的载体就是MeasureSpec
类。
MeasureSpec
我们再来看看MeasureSpec
这个类的定义,这个类首先不保存数据,只是提供构造一个int值,而这个int值包含相应的size和mode,当然也可以通过一个int值来解析出size和mode。
//构造一个int值,包含size和mode
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//获取int值中的mode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
//获取int值中的size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec
的模式分三种,分别是:
- MeasureSpec.AT_MOST
- MeasureSpec.EXACTLY
- MeasureSpec.UNSPECIFIED
第一种模式AT_MOST
,表达的是这种模式下,View的宽高不应当大于给予的宽高(这个宽高就是与mode对应的size啦)。
第二种模式EXACTLY
,表达的是这种模式下,View的宽高应当是个精确值。
第三种模式UNSPECIFIED
,表达的是这种模式下,View的宽高未指定。一般很少用到。
如果你看不懂上面三行在说什么,不要着急,继续往下看。
ViewGroup中的内容
MeasureSpec
类是用来重新处理计算宽高的载体,真正的计算方法在下面这个方法内部,所以我们需要看一下一个ViewGroup
类的方法,measureChildWithMargins
和getChildMeasureSpec
(API=25的源码)。measureChildWithMargins
方法一般在ViewGroup
的具体实现类(比如LinearLayout
,FrameLayout
)的onMeasure
方法中调用,用来重新调整子View的宽高参数。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
我们可以看见在measureChildWithMargins
方法中,传入了五个参数,第一个参数就是ViewGroup
的子View了,接下来的四个参数分别是ViewGroup
自身的宽度MeasureSpec
和高度MeasureSpec
,和已经使用的宽高(被其他子View所使用的尺寸)。然后接下来取出子View自己的LayoutParams
,然后将父View的MeasureSpec
,各种边距,和自己的LayoutParams
中的尺寸传入一个叫childWidthMeasureSpec
的方法中,得到了子View的MeasureSpec
,然后去执行子View自己的measure
方法(measure
方法定义在View类中,是final
方法,不可重写,measure
方法内部调用了onMeasure
方法)。所以在这个方法里面,我们终于探究出父View是如何影响改变子View的尺寸的。接下来我们就看看这个childWidthMeasureSpec
方法是如何具体改变子View的尺寸的。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
......
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
第一个参数是ViewGroup
本身的MeasureSpec
参数,第二个是padding,这里的第三个参数childDimension
就是通过xml文件或者LayoutParams计算得出的子View的宽高,也就是开发者直接写的宽高,比如5dp,100px,或者WRAP_CONTENT
等。
你看看,未处理的参数叫dimension(n.尺寸),已经处理好的参数叫spec(n.规格)。
分三种情况:
- 在父View是
EXACTLY
的情况下,子类的大小childDimension
如果是为“具体的值”(比如5dp)或者MATCH_PARENT
,都将处理为EXACTLY
模式,只不过在“具体的值”情况中size
直接使用了“具体的值”,MATCH_PARENT
情况中size
使用了父View的值,只有在WRAP_CONTENT
的情况下,模式会置为AT_MOST
,同时size
指定为父View的值。- 在父View是
AT_MOST
的情况下,子类的如果是”具体的值”,将为EXACTLY
模式,size同样为”具体的值”,其他的WRAP_CONTENT
和MATCH_PARENT
,都为AT_MOST
模式,size都是父类View的尺寸。- 在父View是
UNSPECIFIED
的情况下,子类的如果是具体的值,将为EXACTLY
模式,其他的WRAP_CONTENT
和MATCH_PARENT
,都为UNSPECIFIED
模式(这个模式先不管他)。
所以如果你读到了这里,估计知晓了EXACTLY
和AT_MOST
的异同,AT_MOST
和WRAP_CONTENT
有比较密切的关系,子View或父View指定的WRAP_CONTENT
的不确定性,使其不能确定为一个精确值,而只能用最大值代替过后,打上AT_MOST
这个标签。而EXACTLY
明显没有这么麻烦,不管你是写5dp也好,还是MATCH_PARENT
也好,都说明你这个尺寸已经确定了,值是“精确的”,没有什么不确定性。
现在你肯定想到了一个问题,既然是父View涉及到指定子View的模式和宽高,那么一层一层往上遍历的话,root层View怎么办呢,他们的模式是什么呢?谁来指定他们呢?然后,你就可以在RootViewImpl
类中发现这个方法getRootMeasureSpec
,得,搞定了。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
要说root层的View就是干脆……MATCH_PARENT
对应EXACTLY
,WRAP_CONTENT
对应AT_MOST
,然后下面的内部的View,就都按照上面说过的getChildMeasureSpec
方法去来了。好,说了这么多,第一部分就结束了。下面我们来看看实际例子,看看onMeasure方法是如何作用在TabLayout
上的。
Part 2
概述
TabLayout
作为支持Material Design的新控件,用来替代ActionBar#Tab
。TabLayout
在样式上具有两种模式,两种模式有着不同的UI效果,这段的主要内容就是分析TabLayout#setTabMode
方法是如何去刷新UI的。
本文源码基于android.support.design.widget扩展包,版本25.0.1。……这个版本有点新。。。
我们先看一下TabLayout中的各种继承关系。仔细看……很重要。
从View的角度来说,TabLayout
继承自HorizontalScrollView
,故内部只能有一个View,这个View就是SlidingTabStrip
,而继承自LinearLayout
的SlidingTabStrip
的内部,有着众多TabView
。
TabMode
/**
* Set the behavior mode for the Tabs in this layout. The valid input options are:
* <ul>
* <li>{@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used
* with content that benefits from quick pivots between tabs.</li>
* <li>{@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment,
* and can contain longer tab labels and a larger number of tabs. They are best used for
* browsing contexts in touch interfaces when users don’t need to directly compare the tab
* labels. This mode is commonly used with a {@link android.support.v4.view.ViewPager}.</li>
* </ul>
*
* @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
*
* @attr ref android.support.design.R.styleable#TabLayout_tabMode
*/
public void setTabMode(@Mode int mode) {
if (mode != mMode) {
mMode = mode;
applyModeAndGravity();
}
}
先翻译一段……
设置Tab在布局中的行为模式,包含以下有效的输入:
- MODE_FIXED: 使用固定模式同时显示所有的Tab,最适合那些通过不同Tab之间来快速切换内容的场景。
- MODE_SCROLLABLE: 在任何给予的场景中通过Tab的子集来显示可滑动的Tab,并且可以包含较长的Tab标签和数量较多的Tab。这种情况最适合那种在触摸界面的浏览环境,但是用户不需要直接比较Tab间的内容。. 通常与 ViewPager配合使用。
TabGravity
在上面的setTabMode
中,调用了applyModeAndGravity
方法,这个方法从名字上来说,似乎和Gravity
也有关系,所以我们又找到了这个方法。
/**
* Set the gravity to use when laying out the tabs.
*
* @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
*
* @attr ref android.support.design.R.styleable#TabLayout_tabGravity
*/
public void setTabGravity(@TabGravity int gravity) {
if (mTabGravity != gravity) {
mTabGravity = gravity;
applyModeAndGravity();
}
}
这里的Gravity,包含了两种有效输入。分别是:
- GRAVITY_FILL
- GRAVITY_CENTER
- 我们先不解释,先来看看效果。
说白了,这两种模式,FIXED的模式就是将每一个Tab分给相同的大小,如果你的Tab特别多,那么估计内容就显示不下了,因为他是不能滚动的。
而SCROLLANBLE模式下,会根据内容来匹配Tab大小,每一个Tab会根据内容自动确定大小,就像WARP_CONTENT一样,在Tab很少的时候可能屏幕上有空缺,Tab很多的情况下就像HorizontalScrollView
那样可以滚动。(实际上TabLayout
就是继承的HorizontalScrollView
哦哈哈)applyModeAndGravity
好,我们发现
setTabMode
方法中实际上只是执行了applyModeAndGravity
方法,那我们下来看一下这个方法。private void applyModeAndGravity() { int paddingStart = 0; if (mMode == MODE_SCROLLABLE) { // If we're scrollable, or fixed at start, inset using padding paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); } ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); switch (mMode) { case MODE_FIXED: mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); break; case MODE_SCROLLABLE: //谷歌真是疯了,兼容性都搞到Gravity里面来了…… mTabStrip.setGravity(GravityCompat.START); break; } updateTabViews(true); }
这个方法中有一个字段让我感到很玄学,
mContentInsetStart
,查遍了各种资料也没能完全理解,根据Api文档的解释,这个的功能是:
- Position in the Y axis from the starting edge that tabs should be positioned from.字面翻译就是:确定在起始位置边界那边Y轴的位置。然而根据上面的方法,这个字段只会在
SCROLLABLE
模式下有效果,用来确定left边缘(也就是start)的一个padding,为了和默认的padding区别开来,需要减去默认的paddingStart,这样的话在加上默认的padding,第一个Tab的内容区域距离View边距刚好就是mContentInsetStart
的长度。这个和api上说的完全不一样好么!!
同时我也发现了另外一个问题,就是我在debug的时候,发现哪怕设置了app:tabPadding="0dp"
,默认的mTabPaddingBottom
和mTabPaddingTop
是有默认值的,都为12dp的长度。但是从UI上却没有边界的效果,不知道是什么原因,还请有知道大佬教授一下。
对于app:tabMaxWidth
,也是同样的情况,除非你设置了非0的值,否则值将置为264dp。不提这些个乱七八糟的了,在设置padding之后,
applyModeAndGravity
方法接下来就是设置gravity,两种模式分别对应两种效果,FIXED
为center_horizontal,SCROLLABLE
为start,这个也可以从UI上直接看出来。这里要提一下,出现了一个叫mTabStrip
的变量,这个是TabLayout的一个内部类,TabLayout#SlidingTabStrip
,因为TabLayout继承自HorizontalScrollView
,只能有一个子View,所以需要一个子View来包含所有的子View,故mTabStrip
这个变量就是真正的TabLayout的内容区域的View类,包括了所有Tab的View。TabLayout#SlidingTabStrip
继承LinearLayout
,这个也是容易理解,不再赘述。updateTabViews
然后就是
updateTabViews(final boolean requestLayout)
方法了,我们继续看。void updateTabViews(final boolean requestLayout) { for (int i = 0; i < mTabStrip.getChildCount(); i++) { View child = mTabStrip.getChildAt(i); child.setMinimumWidth(getTabMinWidth()); updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); if (requestLayout) { child.requestLayout(); } } } private int getTabMinWidth() { if (mRequestedTabMinWidth != INVALID_WIDTH) { // If we have been given a min width, use it return mRequestedTabMinWidth; } // Else, we'll use the default value return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; }
在updateTabViews方法中,刷新了每个Tab的LayoutParams,同时设置了最小宽度
可以看到在getTabMinWidth
方法中,mScrollableTabMinWidth
这个值取自mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);
默认的值为72dp,而
mRequestedTabMinWidth
即为app:tabMinWidth
属性设置的值。所以最小宽度的结论是,如果设置过app:tabMinWidth
,则mRequestedTabMinWidth
就不等于默认设置的INVALID_WIDTH
,直接返回设置的值,如果没有设置的话,根据当前的模式,如果为SCROLLABLE
的话,为给定的mScrollableTabMinWidth
(72dp),如果为FIXED
的话,为0。扯了这么多没用的……接下来可能就有点复杂了,因为
updateTabViews
方法会根据传入的布尔值,来决定是否刷新布局,也就是执行requestLayout
方法。requestLayout
方法会根据当前的情况,从目标节点开始往上遍历……所以这约等于刷新了整个布局……,好,我们就来看一下各种measure方法和layout方法。我们发现除了TabLayout的类以外,还有两个内部类也是值得关注的,分别是
SlidingTabStrip
和TabView
,他们分别代表所有Tab的一个容器和单个Tab的内容视图。其中只有SlidingTabStrip
复写了onLayout
方法,接下来我们逐一来关注这些方法。TabLayout#onMeasure
我们首先关注
TabLayout
自身的onMeasure
方法。@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // If we have a MeasureSpec which allows us to decide our height, try and use the default // height final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.AT_MOST: heightMeasureSpec = MeasureSpec.makeMeasureSpec( Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.EXACTLY); break; case MeasureSpec.UNSPECIFIED: heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); break; } final int specWidth = MeasureSpec.getSize(widthMeasureSpec); if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { mTabMaxWidth = mRequestedTabMaxWidth > 0 ? mRequestedTabMaxWidth : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); } // Now super measure itself using the (possibly) modified height spec super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() == 1) { // If we're in fixed mode then we need to make the tab strip is the same width as us // so we don't scroll final View child = getChildAt(0); boolean remeasure = false; switch (mMode) { case MODE_SCROLLABLE: remeasure = child.getMeasuredWidth() < getMeasuredWidth(); break; case MODE_FIXED: // Resize the child so that it doesn't scroll remeasure = child.getMeasuredWidth() != getMeasuredWidth(); break; } if (remeasure) { int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), child.getLayoutParams().height); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( getMeasuredWidth(), MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
我们慢慢详细的看,首先5-15行,第五行有个变量
idealHeight
,这个变量调用了一个方法getDefaultHeight()
,我们也看一下。private int getDefaultHeight() { boolean hasIconAndText = false; for (int i = 0, count = mTabs.size(); i < count; i++) { Tab tab = mTabs.get(i); if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { hasIconAndText = true; break; } } return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; }
getDefaultHeight()
返回了一个int值,这个值在存在某一个Tab同时存在icon和text的时候,会将返回值置成DEFAULT_HEIGHT_WITH_TEXT_ICON
,如果每一个Tab都不符合这种情况,则还是默认返回DEFAULT_HEIGHT
。DEFAULT_HEIGHT_WITH_TEXT_ICON
和DEFAULT_HEIGHT
为TabLayout
中的常量,值分别为72dp和48dp。所以到这个地方,我们也能看出来
idealHeight
是什么意思了,ideal英文翻译为理想的,这个变量就是理想高度,有icon的时候调整为72dp,没有的时候默认为48dp。呃,当然还要加上顶部和底部的padding。。
接下来一个switch,可能需要解释的有点多了,为了方便查看,我单独摘取出来。switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.AT_MOST: heightMeasureSpec = MeasureSpec.makeMeasureSpec( Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.EXACTLY); break; case MeasureSpec.UNSPECIFIED: heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); break; }
这里的
heightMeasureSpec
,虽然只是一个int值,但是可以通过拆解来获得一个mode和一个size,同理widthMeasureSpec
也一样,都可以通过MeasureSpec
类的getMode
和getSize
来获得,这里如果高度的size为MeasureSpec.AT_MOST
模式的时候,则将刚才获取的idealHeight
(理想高度)和本身的高度相比较,去较小者重新放入heightMeasureSpec
,同时将模式指定为EXACTLY
。switch case的第一个条件,如果TabLayout的高度因为自己或者父View设置的
WRAP_CONTENT
,造成TabLayout的父View在处理TabLayout的时候,将模式置成AT_MOST
(原理参见Part1),这个时候表示高度的size+mode还不能确定为一个精确值,所以需要确定下来,故置为EXACTLY
模式。第二个条件的UNSPECIFIED
极少用到,这里先略过,我们只需要知道这个时候高度也置成了idealHeight
(理想高度)。所以总结来说,当你编写xml文件的时候将高度设置成
WRAP_CONTENT
,或者自身是MATCH_PARENT
而父View或者祖先View的高度设置成了WRAP_CONTENT
,会造成高度不确定的AT_MOST
模式,故在onMeasure
方法内处理了这种情况,如果你的Tab存在有icon的情况,则将理想高度置成72dp,普通情况下为48dp。所以大家可以用WRAP_CONTENT
试一试,高度是不是这个高度。我们终于过了
TabLayout#onMeasure
方法的5到15行……,接下来我们看17-27行。final int specWidth = MeasureSpec.getSize(widthMeasureSpec); if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { // If we don't have an unspecified width spec, use the given size to calculate // the max tab width mTabMaxWidth = mRequestedTabMaxWidth > 0 ? mRequestedTabMaxWidth : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); } // Now super measure itself using the (possibly) modified height spec super.onMeasure(widthMeasureSpec, heightMeasureSpec);
有了上面对
MeasureSpec
的解释,这个看上去会好很多。这次取出了宽度的模式,如果不为UNSPECIFIED
的话(一般情况下都不会为这个)则去设置了mTabMaxWidth
的值。另外一个与其类似的值mRequestedTabMaxWidth
,则就是xml文件中的app:tabMaxWidth
具体的值。如果你在xml中设置了不为零的值的话,那么mTabMaxWidth
就会为设置的值,如果没有设置,就为宽度的size大小减去56dp(TAB_MIN_WIDTH_MARGIN=56),这个mTabMaxWidth
有什么用,我们下面会讲到。我们上一张图,字太多了有点累。
我们继续看,在
TabLayout#onMeasure
方法的第27行,执行了父类的onMeasure
方法,由于TabLayout
继承的HorizontalScrollView
,其mFillViewport
属性为false
,所以super.onMeasure
就直接return
,故HorizontalScrollView
的逻辑跳过,来到了父类的父类FrameLayout
中的onMeasure
方法。
HorizontalScrollView#onMeasure
(API=25)@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!mFillViewport) { return; } } ......
可以看见……就只是执行了super的方法就被return了。接下来就是
FrameLayout#onMeasure
方法了。由于FrameLayout#onMeasure
方法内部逻辑比较复杂,这里就只贴一部分,其他的我都省略了。@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); int maxHeight = 0; int maxWidth = 0; int childState = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); …… childState = combineMeasuredStates(childState, child.getMeasuredState()); …… } } setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); …… }
这里会涉及到很多新概念新东西,我们尽量忽略一些和本篇无关的东西。这里在第七行引入了一个叫
childState
的变量,包括下面的resolveSizeAndState
方法,这个涉及到一些新概念,这里就不详细说了,如果有读者想了解这一段,这篇博文解释了这里面的内容: 源码解析Android中View的measure量算过程。
当子View的可视状态不为GONE
时,调用了measureChildWithMargins
,这个方法我们在前面提到了,这是测量子View的一个开端。然后下面就是调用了View#setMeasuredDimension
方法,View#setMeasuredDimension
这个方法定义在View类中,意味着一轮测量的结束,这个方法调用之后,就能通过getMeasuredHeight
和getMeasuredWidth
来获取宽高。(注意measureChildWithMargins
是去测量子View,View#setMeasuredDimension
则是测量View本身。)我们使用最简单的情况,目前已经已经强制将
TabLayout
的heightMeasureSpec
置为了EXACTLY
模式,所以如果我们的widthMeasureSpec
为一个具体的值,情况就为简单很多,接下来的分析中,我们就假定TabLayout
及其父类和祖先们的的宽度也为MATCH_PARENT
,这样就也会直接解析成EXACTLY
模式。我们继续往下看,越过了各种
super
的onMeasure
方法,现在我们的TabLayout有了measureWidth
和measureHeight
,那么接下来就简单一点儿了。if (getChildCount() == 1) { final View child = getChildAt(0); boolean remeasure = false; switch (mMode) { case MODE_SCROLLABLE: remeasure = child.getMeasuredWidth() < getMeasuredWidth(); break; case MODE_FIXED: remeasure = child.getMeasuredWidth() != getMeasuredWidth(); break; } if (remeasure) { int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), child.getLayoutParams().height); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( getMeasuredWidth(), MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } }
这里的
getChildCount()
获取了如果只有一个子View(实际上就是TabLayout#SlidingTabStrip
)的情况下(肯定只有一个View啊……),会有一个是否重新测量SlidingTabStrip
的标记,这里都只对宽度做了动作,两种情况:
SCROLLABLE
:SlidingTabStrip
小于TabLayout
的宽度FIXED
:SlidingTabStrip
不等于TabLayout
的宽度- 而重新测量子
SlidingTabStrip
的内部,则将其宽度置成了和TabLayout
一样的宽度。这将在下面的代码中有所反映。TabLayout#SlidingTabStrip#onMeasure
我们已经看完了
TabLayout
的onMeasure
方法,接下来我们看看TabLayout
的唯一一个子View
,SlidingTabStrip
的测量过程。因为
TabLayout
继承自HorizontalScrollView
,故继承自LinearLayout
的SlidingTabStrip
就成为了唯一一个TabLayout
内容区域的View。TabLayout
在构造方法中加入了SlidingTabStrip
类的实例。mTabStrip = new SlidingTabStrip(context); super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
这里我们需要记住一件事情,就是
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT
,这个将决定后面的测量结果。好,我们来看SlidingTabStrip
的onMeasure
方法。@Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //第一段 if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { // HorizontalScrollView will first measure use with UNSPECIFIED, and then with // EXACTLY. Ignore the first call since anything we do will be overwritten anyway return; }
我们先来看第一段,这个地方逻辑倒是不麻烦,首先调用了super方法,因为
SlidingTabStrip
继承自LinearLayout
,所以调用super方法应该是它的UI特性和LinearLayout
相同,实际上最终的UI效果的确验证了这一点,接下来判断如果widthMeasureSpec
的模式不是EXACTLY
的情况下,直接return
,里面的注释写着:HorizontalScrollView
(TabLayout
父类)在第一次测量过程中会将子View置为UNSPECIFIED
,之后才为EXACTLY
,所以忽略不为EXACTLY
的情况。
HorizontalScrollView
因为重写了measureChildWithMargins
方法,所以TabLayout
在测量SlidingTabStrip
的时候会调用HorizontalScrollView#measureChildWithMargins
方法,里面有这样一句代码。final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),MeasureSpec.UNSPECIFIED);
而
MeasureSpec#makeSafeMeasureSpec
的这个方法内部使得size
被置为0,所以我们第一次运行到SlidingTabStrip#onMeasure
的时候,size
和mode
分别是0和UNSPECIFIED
,如果这样的数据最后传给了setMeasuredDimension
方法,那么明显不符合最后的UI表现,所以我们这个时候回到上面对TabLayout#onMeasure
最后一部分的分析,如果在SCROLLABLE
模式下SlidingTabStrip
的measureWidth
小于TabLayout
本身的measureWidth
,或者在FIXED
模式下不等于TabLayout
本身的measureWidth
,则将SlidingTabStrip
直接置为TabLayout
的宽度。//第二段 if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { final int count = getChildCount(); int largestTabWidth = 0; for (int i = 0, z = count; i < z; i++) { View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); } } if (largestTabWidth <= 0) { return; } final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); boolean remeasure = false; if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { for (int i = 0; i < count; i++) { final LinearLayout.LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); if (lp.width != largestTabWidth || lp.weight != 0) { lp.width = largestTabWidth; lp.weight = 0; remeasure = true; } } } else { mTabGravity = GRAVITY_FILL; updateTabViews(false); remeasure = true; } if (remeasure) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } }
我们再来看第二段,第二段的都在一个if条件中,只有在在mode和gravity分别在
MODE_FIXED
和GRAVITY_CENTER
模式下才符合,我们这个时候回去看看那张图。这种模式下Tab
呈现出几个特点。首先是Tab
整个内容区域居中,然后每个Tab
都是相同大小,大小的尺寸为最大的那个Tab
的尺寸。在第二段的7到12行,取得了所有View中最大的那个View的尺寸。赋给了int变量largestTabWidth
。然后在22行,做了一个最极端的情况,,以largestTabWidth
乘上Tab的数量
,与整个TabLayout
的width-32dp
相比较,如果是小于这个值的情况下,则表示以当前的Tab
的情况,可以使用MODE_FIXED
+GRAVITY_CENTER
,则在25行的for循环内,将所有的Tab
全部置为最大的那个Tab值。这样的做法符合上面图中的表现。值得注意的是,在取
TabLayout
的测量宽度的时候,减去了两个16dp,并将变量名取为gutter
,这个做法就是区别一些情况下GRAVITY_FILL
和GRAVITY_CENTER
的差别,GRAVITY_FILL
两边是没有边距的,而GRAVITY_CENTER
不管你掌握的如何好,和两边至少都有16dp的边距。这个在其他设备上还不一定为16dp,在谷歌Material Design网站中,存在这个的一些规范,而且他取的名字也叫gutter。如果不符合上面那种情况呢?比如说有一个
Tab
特别大,然后其他的Tab
又比较小,计算下来超过了TabLayout
的内容宽度,这个时候就将mode
置成GRAVITY_FILL
,同时调用了updateTabViews(false);
去刷新了View。我们回顾一下
TabLayout
和SlidingTabStrip
的测量过程。
- 1:如果开发者在定义
TabLayout
本身的高度时,选择了WRAP_CONTENT
或者因为父View的WRAP_CONTENT
导致在measure阶段使其模式置为非EXACTLY
的模式(AT_MOST
,UNSPECIFIED
),在这种情况下,TabLayout
都将高度指定为一个精确值,根据有icon和没有的情况,分别为72dp和48dp,mode置为EXACTLY
。 - 2:对于
TabLayout
本身的宽度,并没有进行特别的处理,所遵从的规则基于FrameLayout
,具体过程可参见FrameLayout#onMeasure
和View#resolveSizeAndState
方法。 - 3:
SlidingTabStrip
作为TabLayout
的唯一一个子View,在TabLayout
的构造方法中,宽和高分别指定为WRAP_CONTENT
和MATCH_PARENT
。 - 4:
SlidingTabStrip
的高度为MATCH_PARENT
,在TabLayout
的高度size为精确值,mode为EXACTLY
的情况下,应当与TabLayout
本身的高度相同。 - 5:
SlidingTabStrip
继承自LinearLayout
,所遵从的规则基于LinearLayout
,同时存在一个附加条件,因为Tab数量较少或者其他原因时,会根据当前两种mode的实际情况,在TabLayout#onMeasure
方法中将SlidingTabStrip
的widthMeasureSpec
的size置成和TabLayout
相同宽度,mode置为EXACTLY
。 -
TabView
前面的部分描述了
TabLayout
和SlidingTabStrip
的测量内容。然后对于SlidingTabStrip
这样一个继承自LinearLayout
的ViewGroup
,囊括了所有的Tab,实际上,每一个Tab从View层级上来说,都是一个TabView
,这个类继承自LinearLayout
,下面我们就来详细看看这个类。
在使用TabLayout
的过程中,我们一般通过TabLayout#addTab
方法,或者通过ViewPager
,执行TabLayout#setupWithViewPager
方法。无论是上面哪一种方式,最后都是通过TabLayout#addTabView
方法来将一个Tab添加至TabLayout
中。我们来看一下这个方法。private void addTabView(Tab tab) { final TabView tabView = tab.mView; mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs()); } private LinearLayout.LayoutParams createLayoutParamsForTabs() { final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); updateTabViewLayoutParams(lp); return lp; } private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { lp.width = 0; lp.weight = 1; } else { lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; lp.weight = 0; } }
可以看见上面三个方法都是私有方法。我们先来看一下
addTabView
,在这个方法中,将取出了TabView
,然后使用addView
方法将TabView
加入了SlidingTabStrip
中。至于createLayoutParamsForTabs
和updateTabViewLayoutParams
方法,则是给TabView
设置了LayoutParams
,宽度设置为WRAP_CONTENT
,高度设置为MATCH_PARENT
。同时,只有在mode和gravity分别为MODE_FIXED
和GRAVITY_FILL
的情况下,将每一个TabView
的权重设置为1,这也应证了前面那张图的UI表现。@Override public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); final int maxWidth = getTabMaxWidth(); final int widthMeasureSpec; final int heightMeasureSpec = origHeightMeasureSpec; if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED || specWidthSize > maxWidth)) { // If we have a max width and a given spec which is either unspecified or // larger than the max width, update the width spec using the same mode widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST); } else { // Else, use the original width spec widthMeasureSpec = origWidthMeasureSpec; } // Now lets measure super.onMeasure(widthMeasureSpec, heightMeasureSpec); ...... }
这是
TabView#onMeasure
方法。在这个方法里面,当指定的specWidthSize
小于最大宽度时(默认为264dp)或者宽度的模式为UNSPECIFIED
的情况下,将宽度置为最大宽度,模式置为AT_MOST
。然后执行了super方法,再往后的逻辑都是关于TabView
里面TextView
的行数逻辑,因为不影响布局参数,我就没有贴代码了。所以,逻辑下发到TabView
,只有app:tabMaxWidth
属性对TabView
产生了影响。这里上最后一张图,也算结束了这篇文章。这张图片是在mode=”scrollable”,gravity=”fill”下的情况。