MaterialDesign--(6)Toolbar的使用及其源码分析

简介

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又迎来了春天。

基本使用

Toolbar 示例.png

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 的类结构

Toolbar-Structure1.png
Toolbar-Structure2.png

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

先看效果:

actionLayout.gif

如图,设计要求在这里有个收藏的按钮,并且要动画。
解决方案如下:

 第一步,在 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 源码分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,814评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,124评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,814评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,924评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,815评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,562评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,944评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,582评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,859评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,881评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,700评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,493评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,943评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,115评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,413评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,978评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,182评论 2 339

推荐阅读更多精彩内容