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

作者: Anonymous___ | 来源:发表于2017-07-24 19:05 被阅读0次

    简介

    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 源码分析

    相关文章

      网友评论

        本文标题:MaterialDesign--(6)Toolbar的使用及其源

        本文链接:https://www.haomeiwen.com/subject/kjtikxtx.html