简介
Android 3.0之后,Google引入了ActionBar,想统一安卓应用的导航栏样式。但由于ActionBar难以定制,很大程度上限制了开发人员,比如标题文字大小、间距等不易实现个性化,很多开发者放弃了ActionBar的使用,而是使用普通的ViewGroup来封装自己的App Bar,或者使用JakeWharton大神的ActionBarSherlock库。
后来,自2014年Google I/O 上Material Design横空出世后,市场上的应用又逐步趋向了样式的风格统一,support library中很快就出来了Toolbar控件,一个定制化的ViewGroup,来完善ActionBar的使用,App Bar又迎来了春天。
基本使用
1.控件在 v7包下,需要引入 v7包
compile 'com.android.support:appcompat-v7:xx.x.x'
2.让 Activity 继承 AppCompatActivity
MaterialDesign 系列第一篇就讲了,AppCompat 系列是为了兼容而生。我在6.0的手机上,直接继承 Activity 并没有什么区别,不过这里还是建议继承 AppCompatActivity。
3.主题 style 设置为Theme.AppCompat.Light.NoActionBar的主题
4.xml 里面添加 Toolbar 控件
5.在 activity 的 onCreate()方法中,调用 setSupportActionBar()方法,并且传入 toolbar。
其实这一步可以略过,因为setSupportActionBar()只是把 ToolBar 和 Activity绑定起来,让 Activity 的onCreateOptionsMenu()和onOptionsItemSelected()方法作用到 Toolbar 上,其实 Toolbar 可以直接调用inflateMenu()方法和setOnMenuItemClickListener()方法。需要注意的是,如果调用了setSupportActionBar()方法,再调用 Toolbar 的inflateMenu方法是无效的。
Toolbar 的 attrs 属性
<declare-styleable name="Toolbar">
给标题设置 style
<attr name="titleTextAppearance" format="reference"/>
给副标题设置 style
<attr name="subtitleTextAppearance" format="reference"/>
标题文字
<attr name="title"/>
副标题文字
<attr name="subtitle"/>
好像并没有什么卵用的属性,给 Toolbar设置 gravity = center 没有任何效果~
<attr name="android:gravity"/>
设置标题区域的 margin 值
<attr name="titleMargin" format="dimension"/>
不明白为什么要设计这个属性,Toolbar 源码里面titleMargins直接覆盖了titleMargin
<attr name="titleMargins" format="dimension"/>
同上,如果单独设置了titleMarginStart,这个属性优先
<attr name="titleMarginStart" format="dimension"/>
<attr name="titleMarginEnd" format="dimension"/>
<attr name="titleMarginTop" format="dimension"/>
<attr name="titleMarginBottom" format="dimension"/>
contentInset 内容区间距
<attr name="contentInsetStart"/>
<attr name="contentInsetEnd"/>
<attr name="contentInsetLeft"/>
<attr name="contentInsetRight"/>
设置 title(内容区) 和 Navigation 的间距,默认是16.
<attr name="contentInsetStartWithNavigation"/>
设置 title(内容区) 和 Actions 的间距,默认是16.
<attr name="contentInsetEndWithActions"/>
设置 navigationIcon logo menu 的最大高度
<attr name="maxButtonHeight" format="dimension"/>
默认是 Top,给NaviagtionIcon、menuView 设置 LayoutParams.gravity属性
<attr name="buttonGravity">
<flag name="top" value="0x30"/>
<flag name="bottom" value="0x50"/>
</attr>
<attr name="collapseIcon" format="reference"/>
给盲人用的,一般开发用不到
<attr name="collapseContentDescription" format="string"/>
menu 弹出框 style
<attr name="popupTheme"/>
导航 icon
<attr name="navigationIcon" format="reference"/>
给盲人用的,人声朗读说明
<attr name="navigationContentDescription" format="string"/>
logo 的 icon
<attr name="logo"/>
给盲人用的,人声朗读说明
<attr name="logoDescription" format="string"/>
标题颜色
<attr name="titleTextColor" format="color"/>
副标题颜色
<attr name="subtitleTextColor" format="color"/>
最小高度
<attr name="android:minHeight"/>
</declare-styleable>
Toolbar 的类结构
public 方法比较简单,一般都能顾名思义。
- setTitleMargin 设置标题 margin
- onRtlPropertiesChanged 当布局方向被改变的时候调用。不用管这个方法~
- showOverflowMenu 手动显示 actionMenu 的弹框
- hideOverflowMenu 手动隐藏
- dismissPopupMenus 手动隐藏
- hasExpandedActionView 是否有未展开的 actionView
- collapseActionView 折叠 ActionView
- setTitleTextAppearance 设置标题文本样式
- setSubtitleTextAppearance 设置 subTitle 文本样式
- inflateMenu 添加 menu 菜单,注意不是覆盖
- setContentInsetsRelative 设置内容区左右间距,相对布局方向
- setContentInsetsAbsolute 设置内容区域的绝对位置
- onHoverEvent 鼠标事件。。。。
- generateLayoutParams 获取布局参数
- set/getPopupTheme 设置ActionMenu 弹框风格
- setLogo 设置 logo 图标
- isOverflowMenuShowing 判断溢出菜单是否显示
- set/getTitle 设置获取 title
- set/getSubtitle 设置获取二级标题
- setTitleTextColor 设置标题字体颜色
- setSubtitleTextColor 二级标题字体颜色
- setNavigationIcon 设置导航图标
- setNavigationOnClickListener 设置导航按钮点击事件
- getMenu 获取 menu 对象
- setOverflowIcon 设置溢出菜单点击的 icon 默认是三个白色的小圆点
- setOnMenuItemClickListener 设置菜单条目点击监听
开发中遇到过的一些问题
1.重新设置 menu
在某些产品需求中,我们的 menu 的 Item 类型是需要根据网络请求的状态或者页面滑动修改 item 的 icon 颜色。这时,我们一般会想到在 Toolbar 里面去找方法重新设置 Menu,然后会找到这个方法‘‘mToolbar.inflateMenu()’’,但是实际效果却是在原有 Menu 条目的基础上添加了新的 Menu。查看源码后发现,mToolbar.inflateMenu()方法没没有移除 Menu 的原有 Item,因此正确的姿势应该是:
Menu menu = mToolbar.getMenu();
menu.clear();
mToolbar.inflateMenu(R.menu.xxx);
2.设置 Navigation和 title 的间距
大概一年多以前,我升级了项目的 sdk 到23(好像是这个数),然后我们的 UI 小姐姐找到了我说,“我们的标题栏和返回键的间距怎么变大了,之前没这么大的,这么大好丑啊 balabala....”,当时我就一脸懵逼,我没改过我们Toolbar 上的 Navigation和 Title 的间距啊,这不是默认的么。然后经过一番查找,终于找到了原因。原来是 Google 的 api 在升级的时候,修改了默认 Navigation和 Title 之间的间距(具体是哪个版本我忘了。。)。正确解决问题的姿势应该是给 Toolbar 节点添加如下属性:
app:contentInsetStartWithNavigation ="56dp"
为什么是56dp 呢,56dp 是个临界值,因为 NavigationView 高度默认是等同于 Toolbar 的高度默认56dp,具体见v7包的<dimen name="abc_action_bar_default_height_material">56dp</dimen>这条属性,然后contentInsetStartWithNavigation这条属性默认是72dp,也就是说,默认 Navigation和 Title 直接有16dp 的间距,因此,设置contentInsetStartWithNavigation的值为56或者56以下,比如设置为0(具体原因看下面的源码分析),都可以让 Navigation和 Title 邻近。
3.标题居中
其实 MaterialDesign 的规范中,Title 都是左对齐的,所以 Toolbar 根本就没有 Api 让 Title 居中显示。but,现在的设计师都是 iOS 风格设计,要求 Android 的 Title 也居中,而且程序员还没法把这个道理跟设计师讲清。没办法,道理讲不清,那就靠实力来擦屁股呗。
看过源码的童鞋都知道 ToolBar 继承自 ViewGroup,然后源码里面会遍历 childView
然后如果有足够的空间,就会显示到 Toolbar 的剩余空间上,具体看下面的源码分析。
<android.support.v7.widget.Toolbar>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:singleLine="true"
android:text="自定义标题"/>
</android.support.v7.widget.Toolbar>
4.溢出菜单 popupwindow 不遮挡 Toolbar
Ui 说溢出菜单弹框不应该遮住 Toolbar,纳尼,这弹框的位置不是 Google 的默认风格嘛,这也要改?
这里贴出一些溢出菜单可能会用到的样式~~
<style name="ToolbarTheme" parent="Theme.AppCompat.Light">
<!-- 更换Toolbar OVerFlow menu icon -->
<item name="actionOverflowButtonStyle">@style/OverFlowIcon</item>
<item name="actionOverflowMenuStyle">@style/OverflowMenuStyle</item>
<!-- 设置 toolbar 溢出菜单的文字的颜色 -->
<item name="android:textColor">@android:color/white</item>
<!-- 设置 显示在toolbar上菜单文字的颜色 -->
<item name="actionMenuTextColor">@android:color/white</item>
<!-- 设置toolbar 弹出菜单的字体大小和溢出菜单文字大小-->
<item name="android:textSize">10sp</item>
</style>
<style name="OverflowMenuStyle" parent="@style/Widget.AppCompat.PopupMenu.Overflow">
<!-- 是否覆盖锚点,默认为true,即盖住Toolbar -->
<item name="overlapAnchor">false</item>
<item name="android:dropDownWidth">wrap_content</item>
<item name="android:paddingRight">5dp</item>
<!-- 弹出层背景颜色 -->
<item name="android:popupBackground">@color/colorPrimary</item>
<!-- 弹出层垂直方向上的偏移,即在竖直方向上距离Toolbar的距离,值为负则会盖住Toolbar -->
<item name="android:dropDownVerticalOffset">3dp</item>
<!-- 弹出层水平方向上的偏移,即距离屏幕左边的距离,负值会导致右边出现空隙 -->
<item name="android:dropDownHorizontalOffset">-8dp</item>
<!-- 设置弹出菜单文字颜色 -->
<item name="android:textColor">@android:color/black</item>
</style>
<style name="OverFlowIcon" parent="Widget.AppCompat.ActionButton.Overflow">
<!--溢出菜单按钮 icon,就是那垂直排列的三个小圆点-->
<item name="android:src">@mipmap/abc_ic_ab_back_mtrl_am_alpha</item>
</style>
5.自定义menu 的 actionLayout
先看效果:
如图,设计要求在这里有个收藏的按钮,并且要动画。
解决方案如下:
第一步,在 menu 文件里面天加 item,并且给item 设置 actionLayout
<item
android:id="@+id/action_collection"
android:icon="@drawable/ic_menu_collection_normal_text"
android:orderInCategory="100"
android:title="@string/action_collection"
app:actionLayout="@layout/menu_collect"
app:showAsAction="always"/>
第二步,创建 layout 文件 menu_coupon_collect
<?xml version="1.0" encoding="utf-8"?
<com.example.admin.materialdesign.widget.CollectionView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/coupon_cv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"/>
第三步,创建自定义 View CollectionView,完成点击时的动画
就是酱紫,给 menu 设置一个自己写的 Layout。点击事件的话需要获取CollectionView,CollectionView的代码如下:
toolbar.inflateMenu(R.menu.toolbar_menu);
View collectionMenu = toolbar.getMenu().findItem(R.id.action_collection);
View menuView = collectionMenu.getActionView();
mCollectionView = (CollectionView) menuView.findViewById(R.id.coupon_cv);
源码分析
还是来一波源码分析吧,这玩意要经常看~~
对照上面那种Toolbar 的示例图,我们知道,ToolBar 就是像是一个从左到右的 LinearLayout,依次是 NavigationIcon、Logo、Title/SubTitle、content 内容区域、actionMenu。
嗯~看起来就是酱紫,如果是我自己来设计一个 ToolBar 并实现相同的功能,大概就是这样了。
通过源码我们可以看到 Toolbar 是一个继承自 ViewGroup 的自定义 View。想起了刚开始学自定义 View 的时候,视频讲师的一句话:自定义View 的三个关键方法 onMeasure、onLayout、onDraw。 如果是继承 View,就要关心 onMeasure、onDraw。如果继承 ViewGroup,则应该关心 onMeasure、onLayout。
这里我们就重点来看看 onMeasure 和 onLayout 方法把
onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 0;
int height = 0;
int childState = 0;
final int[] collapsingMargins = mTempMargins;
final int marginStartIndex;
final int marginEndIndex;
//检测当前布局方向是RTL还是LTR
if (ViewUtils.isLayoutRtl(this)) {
marginStartIndex = 1;
marginEndIndex = 0;
} else { //一般都是LTR,即从左到右的放置子View
//这里是通过marginStartIndex和marginEndIndex为0为1来区分LTR还是RTL
//这里很巧妙,不用进行对0/1的判断,下面直接使用就行了
//也就是说在toolbar右边的索引一直1,左边的索引一直是0,不管他是RTL还是LTR
marginStartIndex = 0;
marginEndIndex = 1;
}
//先开始测量Toolbar中属于系统级别的View
//即:mNavButtonView、mCollapseButtonView、mMenuView、mExpandedActionView、mLogoView等
//具体你可以搜索查看一下addSystemView()方法,该方法加入的都是系统级别的
//这里的navWidth存储的是mNavButtonView和mCollapseButtonView的大小。
//如果mCollapseButtonView存在的话,那么navWidth存储的就是mCollapseButtonView的测量宽度和水平偏移之和
//如果mCollapseButtonView不存在的话,那么navWidth存储的就是mNavButtonView的测量宽度和水平偏移之和
//如果两者都不存在的话,那么navWidth就是他们的初始值0
//注意这里我们分别测量时对于不用位置的系统部件,定义了不同的宽度变量来存储,比如下面的navWidth、menuWidth、titleWidth等
//但是垂直方向上的高度我们只用一个height变量来代表,这是因为Toolbar里面的子View大都以水平方向放置的,而垂直高度只要求出这些
//子View的测量高度中的最大值就行了。注意这里有一个特殊点:title和subTitle.我们知道它俩是垂直并列放置的,所以在下面额外定义了
//一个存储title和subTitle高度总和的变量titleHeight,然后用height和titleHeight中最大的那个作为height的值,就这一点例外而已
//具体的下面的代码会证明我上面的那些话
int navWidth = 0;
//shouldLayout(child)就是child不是null并且不是GONE 并且child的parent是本Toolbar
//mNavButtonView其实就是一个ImageButton
//通过app:navigationIcon来设置或者对应的java方法
if (shouldLayout(mNavButtonView)) {
//这里传入的width是水平方向上已经使用了的大小,因为在下面要调用的measureChildConstrained()方法中
//使用到了getChildMeasureSpec()方法,而该方法中第二个参数需要传入使用的总共大小
measureChildConstrained(mNavButtonView, widthMeasureSpec, width, heightMeasureSpec, 0,
mMaxButtonHeight);
//因为上面方法中调用了child.onMeasure()方法,所以在上面方法执行完毕mNavButtonView就已经被测量完毕了
//此时下面navWidth就计算出该mNavButtonView所占的总共水平位置大小(即它本身的水平大小加上它在水平方向上的margin偏移量)
navWidth = mNavButtonView.getMeasuredWidth() + getHorizontalMargins(mNavButtonView);
//此时height变量第一次使用,即它此时的值就是其初始值0,所以此时height就是mNavButtonView所占的总共竖直位置大小(
//即它本身的竖直大小加上它在竖直方向上的margin偏移量)
//在下面每个View中最后都要执行下面这句话,目的就是记录下所有View中最大的高度值
height = Math.max(height, mNavButtonView.getMeasuredHeight() +
getVerticalMargins(mNavButtonView));
//更新测量状态,具体没看懂。。。。。。
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(mNavButtonView));
//到这里mNavButtonView就测量完毕了。下面类似的还要进行测量其他级别为系统级别的View
}
//同上
if (shouldLayout(mCollapseButtonView)) {
measureChildConstrained(mCollapseButtonView, widthMeasureSpec, width,
heightMeasureSpec, 0, mMaxButtonHeight);
//类似上面。我们注意这里仍然使用的是navWidth变量。说明mCollapseButtonView和mNavButtonView
//有联系,具体我现在还不知道
navWidth = mCollapseButtonView.getMeasuredWidth() +
getHorizontalMargins(mCollapseButtonView);
height = Math.max(height, mCollapseButtonView.getMeasuredHeight() +
getVerticalMargins(mCollapseButtonView));
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(mCollapseButtonView));
}
//关于getContentInsetStart() 可以看下面链接处 defaultValue=16dp
// http://stackoverflow.com/questions/26455027/android-api-21-toolbar-padding
//可以通过app:contentInsetStart="0dp"来将其默认的16dp改为0dp
//如果设置了为0dp,那么下面这里getContentInsetStart()返回值就是0
final int contentInsetStart = getContentInsetStart();
//因为width代表的是toolbar空间中目前已经使用了的水平大小
//所以这里取contentInsetStart和navWidth两者中最大的来作为现在水平方向上已使用的宽度大小
//如果你设置了mNavButtonView的话,并且设置contentInsetStart为0dp;那么此时width就等于mNavButtonView
//的宽度,如果你没有设置mNavButtonView的话,并且设置contentInsetStart为0dp,那么此时width就等于0
width += Math.max(contentInsetStart, navWidth);
//下面这里就是直接使用marginStartIndex,而不用判断,
collapsingMargins[marginStartIndex] = Math.max(0, contentInsetStart - navWidth);
//上面计算完了Toolbar左边的NavigationView,并和ToolBar左边的inset做比较取出最大值作为现在已经占用了的水平宽度
//那么接下来就要计算ToolBar右边的menu,mExpandedActionView的大小。测量流程和上面一样。上面是测量Toolbar左边,接
//下来测量右边。两边都测量完了就开始测量中间那些title subTitle view 等东西。如果没位置了中间那些就不放置了
int menuWidth = 0;
//mMenuView其实就是一个ActionMenuView
if (shouldLayout(mMenuView)) {
//和上面同理。不同的是此时width不为0(前提是getContentInsetStart不为0)
measureChildConstrained(mMenuView, widthMeasureSpec, width, heightMeasureSpec, 0,
mMaxButtonHeight);
menuWidth = mMenuView.getMeasuredWidth() + getHorizontalMargins(mMenuView);
height = Math.max(height, mMenuView.getMeasuredHeight() +
getVerticalMargins(mMenuView));
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(mMenuView));
}
//这个和上面的getContentInsetStart()类似
final int contentInsetEnd = getContentInsetEnd();
//此时仍然往width上面加,加上contentInsetEnd,menuWidth中最大的。
//这里为什么要与contentInsetEnd作比较 我的理解是menu是在最后的,我们可以与上面mNavButtonView类比一下
//上面mNavButtonView是在前面的,即start开始位置,所以它是与contentInsetStart做比较的,同理mMenuView的
//位置和mNavButtonView相对的,即在end结束位置,所以他要与contentInsetEnd作比较
width += Math.max(contentInsetEnd,menuWidth);
collapsingMargins[marginEndIndex] = Math.max(0, contentInsetEnd - menuWidth);
//mExpandedActionView其实就是一个View
if (shouldLayout(mExpandedActionView)) {
//注意这里是+= 不是= 。而且这里用的是width 不是 menuWidth 等其他的。
width += measureChildCollapseMargins(mExpandedActionView, widthMeasureSpec, width,
heightMeasureSpec, 0, collapsingMargins);
height = Math.max(height, mExpandedActionView.getMeasuredHeight() +
getVerticalMargins(mExpandedActionView));
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(mExpandedActionView));
}
//mLogoView其实就是一个ImageView
//到这里,那些Toolbar左边这里那些必要navigationView等以及右边的menuView等都已经测量完毕了。
//然后开始放置左边的logoView,你可以通过toolbar.setLogo()来设置
//这里注意测量顺序,测量顺序就代表了对应View在toolbar中的级别。级别越高的越在前面,先要保证级别高的要放下来
//剩下的如果没位置了那些级别低的放不放的下不所谓、
if (shouldLayout(mLogoView)) {
//Title 和 SubTitle左边的ImageView
width += measureChildCollapseMargins(mLogoView, widthMeasureSpec, width,
heightMeasureSpec, 0, collapsingMargins);
height = Math.max(height, mLogoView.getMeasuredHeight() +
getVerticalMargins(mLogoView));
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(mLogoView));
}
final int childCount = getChildCount();
//接下来开始将那些ViewType为CUSTOM的测量进去。这里使用for循环时,里面所有的View都会被
//找到,当然包括那些在上面已经测量过的那些级别级别比较高的View。所以此时在循环内部使用了if
//条件语句来将那些上面测量过的View踢出去。
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mViewType != LayoutParams.CUSTOM || !shouldLayout(child)) {
//ViewType为SYSTEM的在上面已经测量过了 这里开始计算ViewType为CUSTOM的宽度之和
//注意logoView。titleView。subTitleView都是级别为SYSTEM的。具体可以搜索addSystemView()方法
continue;
}
//这里用的是+= ,用来计算总共的宽度,当width特别大时也没事。在getChildMeasureSpec()方法中会有筛选的
width += measureChildCollapseMargins(child, widthMeasureSpec, width,
heightMeasureSpec, 0, collapsingMargins);
height = Math.max(height, child.getMeasuredHeight() + getVerticalMargins(child));
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(child));
}
//for循环执行完毕后计算出了所占的所有宽度和这些View中最大的高度
//因为上面的for循环时测量求出了所有类型为CUSTOM的总宽度。而没有包括titleView、subTitleView等级别为SYSTEM
//的View。所以下面要开始测量了。这里我们从测量顺序来看就应该懂了,尽管titleView、subTitleView等级别为SYSTEM,
//但是他们还是放在了级别为CUSTOM的View之后来测量的(之后测量的缺点就是万一前面的View已经占满了父布局的空间,那么后面的
// 就没地方放了,size就为0)。
//另一个 不同的就是titleView、subTitleView是垂直放置在一列的,所以他们的高度之和要与上面计算出的height作比较,取最大值
int titleWidth = 0;
int titleHeight = 0;
final int titleVertMargins = mTitleMarginTop + mTitleMarginBottom;
final int titleHorizMargins = mTitleMarginStart + mTitleMarginEnd;
//mTitleTextView其实就是一个TextView
if (shouldLayout(mTitleTextView)) {
//这里把值赋给titleWidth没用,因为底下会再次给它赋值
//所以下面这句的意思就是测量mTitleTextView而已
titleWidth = measureChildCollapseMargins(mTitleTextView, widthMeasureSpec,
width + titleHorizMargins, heightMeasureSpec, titleVertMargins,
collapsingMargins);
//上面测量完毕了,所以这里可以拿到它测量之后的结果
titleWidth = mTitleTextView.getMeasuredWidth() + getHorizontalMargins(mTitleTextView);
titleHeight = mTitleTextView.getMeasuredHeight() + getVerticalMargins(mTitleTextView);
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(mTitleTextView));
}
//shouldLayout(child)就是child不是null并且不是GONE 并且child的parent是本Toolbar
//mSubtitleTextView其实就是一个TextView
if (shouldLayout(mSubtitleTextView)) {
//title部分包括了titleWidth和subTitle.而他们是在垂直方向上放置的。所以title的总宽度就是两者
//的宽度的最大值,而高度就是两者高度之和
titleWidth = Math.max(titleWidth, measureChildCollapseMargins(mSubtitleTextView,
widthMeasureSpec, width + titleHorizMargins,
heightMeasureSpec, titleHeight + titleVertMargins,
collapsingMargins));
//注意是 +=,这里就求出垂直方向上的总和
titleHeight += mSubtitleTextView.getMeasuredHeight() +
getVerticalMargins(mSubtitleTextView);
childState = ViewUtils.combineMeasuredStates(childState,
ViewCompat.getMeasuredState(mSubtitleTextView));
}
//这里往所有已经占了的width中加上刚刚计算出来的title所占的宽度
width += titleWidth;
//同样的,高度一直是取最大值
height = Math.max(height, titleHeight);
//最后通知测量完毕toolbar之前把toolbar的padding加进去
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
//下面开始计算水平和竖直方向上的MeasureSpec
//注意我们在FrameLayout中时,是要把计算出来的大小与前景和背景作比较取最大值的
//这里我们只与背景作对比,因为Toolbar没有前景。
final int measuredWidth = ViewCompat.resolveSizeAndState(
Math.max(width, getSuggestedMinimumWidth()),
widthMeasureSpec, childState & ViewCompat.MEASURED_STATE_MASK);
//同上
final int measuredHeight = ViewCompat.resolveSizeAndState(
Math.max(height, getSuggestedMinimumHeight()),
heightMeasureSpec, childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
//通知Toolbar的父布局我这里已经测量完toolbar了 并且把测量后的width height 的 MeasureSpec传入
//这里的shouldCollapse()可能是在使用了CollapsingToolbarLayout才会为真的,否则其他时候为假
setMeasuredDimension(measuredWidth, shouldCollapse() ? 0 : measuredHeight);
}
onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
//此时拿到toolbar的宽度和高度。一般来说在measure之后,getMeasuredWidth和getWidth()值是一样的
//getHeight()同理
//而getWidth()和getHeight()在onLayout()方法开始执行时就可以用来获取宽度和高度了,并且等
// 于getMeasuredWidth/getMeasuredHeight,如果都完成了赋值,两者值是相同的
//而getMeasuredWidth/getMeasuredHeight它的赋值在View的setMeasuredDimension中
final int width = getWidth();
final int height = getHeight();
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int left = paddingLeft;
int right = width - paddingRight;
final int[] collapsingMargins = mTempMargins;
collapsingMargins[0] = collapsingMargins[1] = 0;
// Align views within the minimum toolbar height, if set.
final int alignmentHeight = ViewCompat.getMinimumHeight(this);
//和上面onMeasure()中一样,先操作那些级别高的子View
if (shouldLayout(mNavButtonView)) {
if (isRtl) { //从右到左排列
right = layoutChildRight(mNavButtonView, right, collapsingMargins,
alignmentHeight);
} else { //从左到右排列
left = layoutChildLeft(mNavButtonView, left, collapsingMargins,
alignmentHeight);
}
}
// 同上
if (shouldLayout(mCollapseButtonView)) {
if (isRtl) {
right = layoutChildRight(mCollapseButtonView, right, collapsingMargins,
alignmentHeight);
} else {
left = layoutChildLeft(mCollapseButtonView, left, collapsingMargins,
alignmentHeight);
}
}
// 同上
if (shouldLayout(mMenuView)) {
if (isRtl) {
left = layoutChildLeft(mMenuView, left, collapsingMargins,
alignmentHeight);
} else {
right = layoutChildRight(mMenuView, right, collapsingMargins,
alignmentHeight);
}
}
//这里要给collapsingMargins赋值可能的原因:我们知道在toolbar收缩拉伸时,
//其左上角和右上角的是不变的,而其他位置比如title是会随着拉伸而下移的。
collapsingMargins[0] = Math.max(0, getContentInsetLeft() - left);
collapsingMargins[1] = Math.max(0, getContentInsetRight() - (width - paddingRight - right));
left = Math.max(left, getContentInsetLeft());
//这里是right的坐标,即水平方向的偏移量,right越小,width越小。
right = Math.min(right, width - paddingRight - getContentInsetRight());
//同上
if (shouldLayout(mExpandedActionView)) {
if (isRtl) {
right = layoutChildRight(mExpandedActionView, right, collapsingMargins,
alignmentHeight);
} else {
left = layoutChildLeft(mExpandedActionView, left, collapsingMargins,
alignmentHeight);
}
}
//同上
if (shouldLayout(mLogoView)) {
if (isRtl) {
right = layoutChildRight(mLogoView, right, collapsingMargins,
alignmentHeight);
} else {
left = layoutChildLeft(mLogoView, left, collapsingMargins,
alignmentHeight);
}
}
final boolean layoutTitle = shouldLayout(mTitleTextView);
final boolean layoutSubtitle = shouldLayout(mSubtitleTextView);
int titleHeight = 0;
if (layoutTitle) {
final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
titleHeight += lp.topMargin + mTitleTextView.getMeasuredHeight() + lp.bottomMargin;
}
if (layoutSubtitle) {
final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
titleHeight += lp.topMargin + mSubtitleTextView.getMeasuredHeight() + lp.bottomMargin;
}
//上面求出了title 和 subTitle 的总共height。
if (layoutTitle || layoutSubtitle) {
int titleTop;
final View topChild = layoutTitle ? mTitleTextView : mSubtitleTextView;
final View bottomChild = layoutSubtitle ? mSubtitleTextView : mTitleTextView;
final LayoutParams toplp = (LayoutParams) topChild.getLayoutParams();
final LayoutParams bottomlp = (LayoutParams) bottomChild.getLayoutParams();
final boolean titleHasWidth = layoutTitle && mTitleTextView.getMeasuredWidth() > 0
|| layoutSubtitle && mSubtitleTextView.getMeasuredWidth() > 0;
//下面就根据title的gravity来放置
//重点还是求出titleTop
switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) {
case Gravity.TOP:
titleTop = getPaddingTop() + toplp.topMargin + mTitleMarginTop;
break;
default:
case Gravity.CENTER_VERTICAL:
//现在title剩下的高度,即现在剩下了多高的位置来让他放置title
final int space = height - paddingTop - paddingBottom;
//将title在整个toolbar中可用的高度减去title需要占用的高度就是它用不到剩下的高度
//这里将其除是为了实现将title部分居中在toolbar这块位置中来显示
//而spaceAbove就是title部分在toolbar的paddingTop之后,还需要再对于顶部的偏移
//计算这些都是因为要实现将title放在toolbar的垂直方向的中间。
int spaceAbove = (space - titleHeight) / 2;
//此时开始考虑childView的相对于顶部的margin。对比上面计算(不考虑childView的topMargin和mTitleMarginTop情况下)出
// 来spaceAbove,和它的topMargin和mTitleMarginTop之和,最终选两者中的最大值
if (spaceAbove < toplp.topMargin + mTitleMarginTop) {
spaceAbove = toplp.topMargin + mTitleMarginTop;
} else {
//开始计算title相对于底部的偏移大小
//这里我们分析下下面的代码:
// height - paddingBottom - titleHeight -spaceAbove - paddingTop
//上式等价于:space-titleHeight -spaceAbove(你可以看一下上面的space是怎么算出来的)
//也就等价于 spaceAbove*2-spaceAbove(你可以看一下上面的spaceAbove是怎么算出来的)
// 也就是等价于:spaceAbove;所以说 spaceBelow=spaceAbove;
//但是上面的推理只限于if (spaceAbove < toplp.topMargin + mTitleMarginTop)条件不成立的情况下
//其中titleHeight就是title的总共高度
//看到这里应该就懂了spaceBelow的计算方法了吧。
final int spaceBelow = height - paddingBottom - titleHeight -
spaceAbove - paddingTop;
//如果下面的条件成立的话,就是说child的bottomMargin和mTitleMarginBottom值导致
//如果按原来那样分配空间的话就会超出。所以此时将spaceAbove减少一些,
//注意这里我们的目标是计算出titleTop就行。
if (spaceBelow < toplp.bottomMargin + mTitleMarginBottom) {
spaceAbove = Math.max(0, spaceAbove -
(bottomlp.bottomMargin + mTitleMarginBottom - spaceBelow));
}
}
//计算出titleTop值,
titleTop = paddingTop + spaceAbove;
break;
case Gravity.BOTTOM:
//以下端对齐的方式的话,比较好算一点。光减去底下要偏移的就行,剩下的就是顶部要偏移的
titleTop = height - paddingBottom - bottomlp.bottomMargin - mTitleMarginBottom -
titleHeight;
break;
}
if (isRtl) {
final int rd = (titleHasWidth ? mTitleMarginStart : 0) - collapsingMargins[1];
right -= Math.max(0, rd);
collapsingMargins[1] = Math.max(0, -rd);
//title
int titleRight = right;
int subtitleRight = right;
if (layoutTitle) {
final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
final int titleLeft = titleRight - mTitleTextView.getMeasuredWidth();
final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight();
//将title layout
mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom);
titleRight = titleLeft - mTitleMarginEnd;
titleTop = titleBottom + lp.bottomMargin;
}
if (layoutSubtitle) {
final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
titleTop += lp.topMargin;
final int subtitleLeft = subtitleRight - mSubtitleTextView.getMeasuredWidth();
final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight();
mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom);
subtitleRight = subtitleRight - mTitleMarginEnd;
titleTop = subtitleBottom + lp.bottomMargin;
}
if (titleHasWidth) {
right = Math.min(titleRight, subtitleRight);
}
} else {
final int ld = (titleHasWidth ? mTitleMarginStart : 0) - collapsingMargins[0];
left += Math.max(0, ld);
collapsingMargins[0] = Math.max(0, -ld);
int titleLeft = left;
int subtitleLeft = left;
if (layoutTitle) {
final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
final int titleRight = titleLeft + mTitleTextView.getMeasuredWidth();
final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight();
mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom);
titleLeft = titleRight + mTitleMarginEnd;
titleTop = titleBottom + lp.bottomMargin;
}
if (layoutSubtitle) {
final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
titleTop += lp.topMargin;
final int subtitleRight = subtitleLeft + mSubtitleTextView.getMeasuredWidth();
final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight();
mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom);
subtitleLeft = subtitleRight + mTitleMarginEnd;
titleTop = subtitleBottom + lp.bottomMargin;
}
if (titleHasWidth) {
left = Math.max(titleLeft, subtitleLeft);
}
}
}
// Get all remaining children sorted for layout. This is all prepared
// such that absolute layout direction can be used below.
//下面开始处理那些级别为CUSTOM的子View
//下你把那些gravity为LEFT的View加入到mTempViews中去。然后调用layoutChildLeft()将其layout
addCustomViewsWithGravity(mTempViews, Gravity.LEFT);
final int leftViewsCount = mTempViews.size();
for (int i = 0; i < leftViewsCount; i++) {
left = layoutChildLeft(mTempViews.get(i), left, collapsingMargins,
alignmentHeight);
}
//同上,不过是加入那些gravity为RIGHT的
addCustomViewsWithGravity(mTempViews, Gravity.RIGHT);
final int rightViewsCount = mTempViews.size();
for (int i = 0; i < rightViewsCount; i++) {
right = layoutChildRight(mTempViews.get(i), right, collapsingMargins,
alignmentHeight);
}
// Centered views try to center with respect to the whole bar, but views pinned
// to the left or right can push the mass of centered views to one side or the other.
addCustomViewsWithGravity(mTempViews, Gravity.CENTER_HORIZONTAL);
final int centerViewsWidth = getViewListMeasuredWidth(mTempViews, collapsingMargins);
final int parentCenter = paddingLeft + (width - paddingLeft - paddingRight) / 2;
final int halfCenterViewsWidth = centerViewsWidth / 2;
int centerLeft = parentCenter - halfCenterViewsWidth;
final int centerRight = centerLeft + centerViewsWidth;
if (centerLeft < left) {
centerLeft = left;
} else if (centerRight > right) {
centerLeft -= centerRight - right;
}
final int centerViewsCount = mTempViews.size();
for (int i = 0; i < centerViewsCount; i++) {
centerLeft = layoutChildLeft(mTempViews.get(i), centerLeft, collapsingMargins,
alignmentHeight);
}
mTempViews.clear();
}
参考:
Toolbar 源码分析