从TabLayout#setTabMode开始谈绘制(Measure)流程

 Android的Measure过程作为View的三部曲(measure,layout,draw)的第一步,承担了View到底宽高是多少这个问题。笔者之前也看过很多文章,但是对其中的关键原理并不完全知晓,大多数博文一上来就花费大量篇幅来描述MeasureSpec这个类,那么这个类到底是做什么呢?它的几种模式又分别是什么意思呢。我们就来看看View的measure过程中,Android系统是如何解决View的宽高问题的。
 (本文不关注其他内容……什么onMeasure啥时候调用啊,measure方法和onMeasure的关系啊,为什么measure方法是final的啊,WindowManagerWindowManager#LayoutParams啊,ViewRootImpl啊,都不管……)

在看接下来一些内容的时候,我推荐阅读一些其他精彩的博客。如果这两篇都不读的话……你估计很难知道我在说什么……

ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解
Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(下)

 本文分为两个部分,第一部分简要概述了measure过程中一些关键的内容。第二部分是将TabLayout作为例子,看看具体怎么做的。

Part 1

概述

通常情况下,我们是在xml文件或者LayoutParams类里面,去指定一个View的宽高,我们一般会写具体的值,或者MATCH_PARENTWRAP_CONTENT,但是,需要注意的一点是,一个View的最终宽高不只是它自己指定的宽高,也要它所属的ViewGroup(就是视图层级上的父View)所决定的。比如说一个View的宽度指定为MATCH_PARENT,如果你的父View是LinearLayout,那么父View根据你这个MATCH_PARENT的标记,判断你需要整个View的宽度,所以如果只有你这一个子View的情况下,宽度就直接等于父View的宽度。但是在其他情况下呢?如果是WRAP_CONTENT呢?程序在处理的时候只能认为这是一个“匹配内容”的标记,而不是它需要的具体宽高,因为真正的逻辑是肯定需要一个具体的值来进行测量的,所以,MeasureSpec类出现了。MeasureSpec类通过一系列准则和方法,重新处理了来自xml文件或者LayoutParams中的size,使得View最终都有一个精确的宽高。

 所以简单来说,我们通常写的宽高,对于Android系统来说只是一个参考值,内部需要一套新规则来重新定义View的宽高,这个规则的载体就是MeasureSpec类。

MeasureSpec

 我们再来看看MeasureSpec这个类的定义,这个类首先不保存数据,只是提供构造一个int值,而这个int值包含相应的size和mode,当然也可以通过一个int值来解析出size和mode。

        //构造一个int值,包含size和mode
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        //获取int值中的mode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        //获取int值中的size
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

MeasureSpec的模式分三种,分别是:
- MeasureSpec.AT_MOST
- MeasureSpec.EXACTLY
- MeasureSpec.UNSPECIFIED

 第一种模式AT_MOST,表达的是这种模式下,View的宽高不应当大于给予的宽高(这个宽高就是与mode对应的size啦)。
 第二种模式EXACTLY,表达的是这种模式下,View的宽高应当是个精确值。
 第三种模式UNSPECIFIED,表达的是这种模式下,View的宽高未指定。一般很少用到。
 如果你看不懂上面三行在说什么,不要着急,继续往下看。

ViewGroup中的内容

这里写图片描述

MeasureSpec类是用来重新处理计算宽高的载体,真正的计算方法在下面这个方法内部,所以我们需要看一下一个ViewGroup类的方法,measureChildWithMarginsgetChildMeasureSpec(API=25的源码)。measureChildWithMargins方法一般在ViewGroup的具体实现类(比如LinearLayout,FrameLayout)的onMeasure方法中调用,用来重新调整子View的宽高参数。

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

 我们可以看见在measureChildWithMargins方法中,传入了五个参数,第一个参数就是ViewGroup的子View了,接下来的四个参数分别是ViewGroup自身的宽度MeasureSpec和高度MeasureSpec,和已经使用的宽高(被其他子View所使用的尺寸)。然后接下来取出子View自己的LayoutParams,然后将父View的MeasureSpec,各种边距,和自己的LayoutParams中的尺寸传入一个叫childWidthMeasureSpec的方法中,得到了子View的MeasureSpec,然后去执行子View自己的measure方法(measure方法定义在View类中,是final方法,不可重写,measure方法内部调用了onMeasure方法)。所以在这个方法里面,我们终于探究出父View是如何影响改变子View的尺寸的。接下来我们就看看这个childWidthMeasureSpec方法是如何具体改变子View的尺寸的。

     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        ......
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

 第一个参数是ViewGroup本身的MeasureSpec参数,第二个是padding,这里的第三个参数childDimension就是通过xml文件或者LayoutParams计算得出的子View的宽高,也就是开发者直接写的宽高,比如5dp,100px,或者WRAP_CONTENT

你看看,未处理的参数叫dimension(n.尺寸),已经处理好的参数叫spec(n.规格)。

分三种情况:

  • 在父View是EXACTLY的情况下,子类的大小childDimension如果是为“具体的值”(比如5dp)或者MATCH_PARENT,都将处理为EXACTLY模式,只不过在“具体的值”情况中size直接使用了“具体的值”,MATCH_PARENT情况中size使用了父View的值,只有在WRAP_CONTENT的情况下,模式会置为AT_MOST,同时size指定为父View的值。
  • 在父View是AT_MOST的情况下,子类的如果是”具体的值”,将为EXACTLY模式,size同样为”具体的值”,其他的WRAP_CONTENTMATCH_PARENT,都为AT_MOST模式,size都是父类View的尺寸。
  • 在父View是UNSPECIFIED的情况下,子类的如果是具体的值,将为EXACTLY模式,其他的WRAP_CONTENTMATCH_PARENT,都为UNSPECIFIED模式(这个模式先不管他)。

 所以如果你读到了这里,估计知晓了EXACTLYAT_MOST的异同,AT_MOSTWRAP_CONTENT有比较密切的关系,子View或父View指定的WRAP_CONTENT的不确定性,使其不能确定为一个精确值,而只能用最大值代替过后,打上AT_MOST这个标签。而EXACTLY明显没有这么麻烦,不管你是写5dp也好,还是MATCH_PARENT也好,都说明你这个尺寸已经确定了,值是“精确的”,没有什么不确定性。

 现在你肯定想到了一个问题,既然是父View涉及到指定子View的模式和宽高,那么一层一层往上遍历的话,root层View怎么办呢,他们的模式是什么呢?谁来指定他们呢?然后,你就可以在RootViewImpl类中发现这个方法getRootMeasureSpec,得,搞定了。

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

 要说root层的View就是干脆……MATCH_PARENT对应EXACTLYWRAP_CONTENT对应AT_MOST,然后下面的内部的View,就都按照上面说过的getChildMeasureSpec方法去来了。好,说了这么多,第一部分就结束了。下面我们来看看实际例子,看看onMeasure方法是如何作用在TabLayout上的。

Part 2

概述

TabLayout作为支持Material Design的新控件,用来替代ActionBar#TabTabLayout在样式上具有两种模式,两种模式有着不同的UI效果,这段的主要内容就是分析TabLayout#setTabMode方法是如何去刷新UI的。

本文源码基于android.support.design.widget扩展包,版本25.0.1。……这个版本有点新。。。
我们先看一下TabLayout中的各种继承关系。仔细看……很重要。
这里写图片描述
从View的角度来说,TabLayout继承自HorizontalScrollView,故内部只能有一个View,这个View就是SlidingTabStrip,而继承自LinearLayoutSlidingTabStrip的内部,有着众多TabView

TabMode

/**
     * Set the behavior mode for the Tabs in this layout. The valid input options are:
     * <ul>
     * <li>{@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used
     * with content that benefits from quick pivots between tabs.</li>
     * <li>{@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment,
     * and can contain longer tab labels and a larger number of tabs. They are best used for
     * browsing contexts in touch interfaces when users don’t need to directly compare the tab
     * labels. This mode is commonly used with a {@link android.support.v4.view.ViewPager}.</li>
     * </ul>
     *
     * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
     *
     * @attr ref android.support.design.R.styleable#TabLayout_tabMode
     */
    public void setTabMode(@Mode int mode) {
        if (mode != mMode) {
            mMode = mode;
            applyModeAndGravity();
        }
    }

 先翻译一段……

设置Tab在布局中的行为模式,包含以下有效的输入:

  • MODE_FIXED: 使用固定模式同时显示所有的Tab,最适合那些通过不同Tab之间来快速切换内容的场景。
  • MODE_SCROLLABLE: 在任何给予的场景中通过Tab的子集来显示可滑动的Tab,并且可以包含较长的Tab标签和数量较多的Tab。这种情况最适合那种在触摸界面的浏览环境,但是用户不需要直接比较Tab间的内容。. 通常与 ViewPager配合使用。

TabGravity

 在上面的setTabMode中,调用了applyModeAndGravity方法,这个方法从名字上来说,似乎和Gravity也有关系,所以我们又找到了这个方法。

    /**
     * Set the gravity to use when laying out the tabs.
     *
     * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
     *
     * @attr ref android.support.design.R.styleable#TabLayout_tabGravity
     */
    public void setTabGravity(@TabGravity int gravity) {
        if (mTabGravity != gravity) {
            mTabGravity = gravity;
            applyModeAndGravity();
        }
    }

 这里的Gravity,包含了两种有效输入。分别是:

  • GRAVITY_FILL
  • GRAVITY_CENTER
  • 我们先不解释,先来看看效果。

    这里写图片描述
     说白了,这两种模式,FIXED的模式就是将每一个Tab分给相同的大小,如果你的Tab特别多,那么估计内容就显示不下了,因为他是不能滚动的。
    而SCROLLANBLE模式下,会根据内容来匹配Tab大小,每一个Tab会根据内容自动确定大小,就像WARP_CONTENT一样,在Tab很少的时候可能屏幕上有空缺,Tab很多的情况下就像HorizontalScrollView那样可以滚动。(实际上TabLayout就是继承的HorizontalScrollView哦哈哈)

    applyModeAndGravity

     好,我们发现setTabMode方法中实际上只是执行了applyModeAndGravity方法,那我们下来看一下这个方法。

        private void applyModeAndGravity() {
            int paddingStart = 0;
            if (mMode == MODE_SCROLLABLE) {
                // If we're scrollable, or fixed at start, inset using padding
                paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
            }
            ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);
    
            switch (mMode) {
                case MODE_FIXED:
                    mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
                    break;
                case MODE_SCROLLABLE:
                    //谷歌真是疯了,兼容性都搞到Gravity里面来了……
                    mTabStrip.setGravity(GravityCompat.START);
                    break;
            }
    
            updateTabViews(true);
        }
    

     这个方法中有一个字段让我感到很玄学,mContentInsetStart,查遍了各种资料也没能完全理解,根据Api文档的解释,这个的功能是:
    - Position in the Y axis from the starting edge that tabs should be positioned from.

     字面翻译就是:确定在起始位置边界那边Y轴的位置。然而根据上面的方法,这个字段只会在SCROLLABLE模式下有效果,用来确定left边缘(也就是start)的一个padding,为了和默认的padding区别开来,需要减去默认的paddingStart,这样的话在加上默认的padding,第一个Tab的内容区域距离View边距刚好就是mContentInsetStart的长度。这个和api上说的完全不一样好么!!
     同时我也发现了另外一个问题,就是我在debug的时候,发现哪怕设置了app:tabPadding="0dp",默认的mTabPaddingBottommTabPaddingTop是有默认值的,都为12dp的长度。但是从UI上却没有边界的效果,不知道是什么原因,还请有知道大佬教授一下。
    什么鬼
     对于app:tabMaxWidth,也是同样的情况,除非你设置了非0的值,否则值将置为264dp。

     不提这些个乱七八糟的了,在设置padding之后,applyModeAndGravity方法接下来就是设置gravity,两种模式分别对应两种效果,FIXED为center_horizontal,SCROLLABLE为start,这个也可以从UI上直接看出来。这里要提一下,出现了一个叫mTabStrip的变量,这个是TabLayout的一个内部类,TabLayout#SlidingTabStrip,因为TabLayout继承自HorizontalScrollView,只能有一个子View,所以需要一个子View来包含所有的子View,故mTabStrip这个变量就是真正的TabLayout的内容区域的View类,包括了所有Tab的View。TabLayout#SlidingTabStrip继承LinearLayout,这个也是容易理解,不再赘述。

    updateTabViews

     然后就是updateTabViews(final boolean requestLayout)方法了,我们继续看。

        void updateTabViews(final boolean requestLayout) {
            for (int i = 0; i < mTabStrip.getChildCount(); i++) {
                View child = mTabStrip.getChildAt(i);
                child.setMinimumWidth(getTabMinWidth());
                updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
                if (requestLayout) {
                    child.requestLayout();
                }
            }
        }
    
        private int getTabMinWidth() {
            if (mRequestedTabMinWidth != INVALID_WIDTH) {
                // If we have been given a min width, use it
                return mRequestedTabMinWidth;
            }
            // Else, we'll use the default value
            return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
        }
    

     在updateTabViews方法中,刷新了每个Tab的LayoutParams,同时设置了最小宽度
    可以看到在getTabMinWidth方法中,mScrollableTabMinWidth这个值取自

    mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);

    默认的值为72dp,而mRequestedTabMinWidth即为app:tabMinWidth属性设置的值。所以最小宽度的结论是,如果设置过app:tabMinWidth,则mRequestedTabMinWidth就不等于默认设置的INVALID_WIDTH,直接返回设置的值,如果没有设置的话,根据当前的模式,如果为SCROLLABLE的话,为给定的mScrollableTabMinWidth(72dp),如果为FIXED的话,为0。

     扯了这么多没用的……接下来可能就有点复杂了,因为updateTabViews方法会根据传入的布尔值,来决定是否刷新布局,也就是执行requestLayout方法。requestLayout方法会根据当前的情况,从目标节点开始往上遍历……所以这约等于刷新了整个布局……,好,我们就来看一下各种measure方法和layout方法。

     我们发现除了TabLayout的类以外,还有两个内部类也是值得关注的,分别是SlidingTabStripTabView,他们分别代表所有Tab的一个容器和单个Tab的内容视图。其中只有SlidingTabStrip复写了onLayout方法,接下来我们逐一来关注这些方法。

    TabLayout#onMeasure

     我们首先关注TabLayout自身的onMeasure方法。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // If we have a MeasureSpec which allows us to decide our height, try and use the default
            // height
            final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom();
            switch (MeasureSpec.getMode(heightMeasureSpec)) {
                case MeasureSpec.AT_MOST:
                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
                            MeasureSpec.EXACTLY);
                    break;
                case MeasureSpec.UNSPECIFIED:
                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY);
                    break;
            }
    
            final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
            if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
                mTabMaxWidth = mRequestedTabMaxWidth > 0
                        ? mRequestedTabMaxWidth
                        : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
            }
    
            // Now super measure itself using the (possibly) modified height spec
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            if (getChildCount() == 1) {
                // If we're in fixed mode then we need to make the tab strip is the same width as us
                // so we don't scroll
                final View child = getChildAt(0);
                boolean remeasure = false;
    
                switch (mMode) {
                    case MODE_SCROLLABLE:
                        remeasure = child.getMeasuredWidth() < getMeasuredWidth();
                        break;
                    case MODE_FIXED:
                        // Resize the child so that it doesn't scroll
                        remeasure = child.getMeasuredWidth() != getMeasuredWidth();
                        break;
                }
    
                if (remeasure) {
                    int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
                            + getPaddingBottom(), child.getLayoutParams().height);
                    int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            getMeasuredWidth(), MeasureSpec.EXACTLY);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                }
            }
        }

     我们慢慢详细的看,首先5-15行,第五行有个变量idealHeight,这个变量调用了一个方法getDefaultHeight(),我们也看一下。

        private int getDefaultHeight() {
            boolean hasIconAndText = false;
            for (int i = 0, count = mTabs.size(); i < count; i++) {
                Tab tab = mTabs.get(i);
                if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) {
                    hasIconAndText = true;
                    break;
                }
            }
            return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT;
        }

    getDefaultHeight()返回了一个int值,这个值在存在某一个Tab同时存在icon和text的时候,会将返回值置成DEFAULT_HEIGHT_WITH_TEXT_ICON,如果每一个Tab都不符合这种情况,则还是默认返回DEFAULT_HEIGHT

    DEFAULT_HEIGHT_WITH_TEXT_ICONDEFAULT_HEIGHTTabLayout中的常量,值分别为72dp和48dp。

     所以到这个地方,我们也能看出来idealHeight是什么意思了,ideal英文翻译为理想的,这个变量就是理想高度,有icon的时候调整为72dp,没有的时候默认为48dp。呃,当然还要加上顶部和底部的padding。。
     接下来一个switch,可能需要解释的有点多了,为了方便查看,我单独摘取出来。

            switch (MeasureSpec.getMode(heightMeasureSpec)) {
                case MeasureSpec.AT_MOST:
                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
                            MeasureSpec.EXACTLY);
                    break;
                case MeasureSpec.UNSPECIFIED:
                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY);
                    break;
            }

     这里的heightMeasureSpec,虽然只是一个int值,但是可以通过拆解来获得一个mode和一个size,同理widthMeasureSpec也一样,都可以通过MeasureSpec类的getModegetSize来获得,这里如果高度的size为MeasureSpec.AT_MOST模式的时候,则将刚才获取的idealHeight(理想高度)和本身的高度相比较,去较小者重新放入heightMeasureSpec,同时将模式指定为EXACTLY

     switch case的第一个条件,如果TabLayout的高度因为自己或者父View设置的WRAP_CONTENT,造成TabLayout的父View在处理TabLayout的时候,将模式置成AT_MOST(原理参见Part1),这个时候表示高度的size+mode还不能确定为一个精确值,所以需要确定下来,故置为EXACTLY模式。第二个条件的UNSPECIFIED极少用到,这里先略过,我们只需要知道这个时候高度也置成了idealHeight(理想高度)。

    所以总结来说,当你编写xml文件的时候将高度设置成WRAP_CONTENT,或者自身是MATCH_PARENT而父View或者祖先View的高度设置成了WRAP_CONTENT,会造成高度不确定的AT_MOST模式,故在onMeasure方法内处理了这种情况,如果你的Tab存在有icon的情况,则将理想高度置成72dp,普通情况下为48dp。所以大家可以用WRAP_CONTENT试一试,高度是不是这个高度。

     我们终于过了TabLayout#onMeasure方法的5到15行……,接下来我们看17-27行。

            final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
            if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
                // If we don't have an unspecified width spec, use the given size to calculate
                // the max tab width
                mTabMaxWidth = mRequestedTabMaxWidth > 0
                        ? mRequestedTabMaxWidth
                        : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
            }
    
            // Now super measure itself using the (possibly) modified height spec
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);

     有了上面对MeasureSpec的解释,这个看上去会好很多。这次取出了宽度的模式,如果不为UNSPECIFIED的话(一般情况下都不会为这个)则去设置了mTabMaxWidth的值。另外一个与其类似的值mRequestedTabMaxWidth,则就是xml文件中的app:tabMaxWidth具体的值。如果你在xml中设置了不为零的值的话,那么mTabMaxWidth就会为设置的值,如果没有设置,就为宽度的size大小减去56dp(TAB_MIN_WIDTH_MARGIN=56),这个mTabMaxWidth有什么用,我们下面会讲到。我们上一张图,字太多了有点累。
    这里写图片描述

     我们继续看,在TabLayout#onMeasure方法的第27行,执行了父类的onMeasure方法,由于TabLayout继承的HorizontalScrollView,其mFillViewport 属性为false,所以super.onMeasure就直接return,故HorizontalScrollView的逻辑跳过,来到了父类的父类FrameLayout中的onMeasure方法。
    HorizontalScrollView#onMeasure(API=25)

      @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            if (!mFillViewport) {
                return;
            }
        }
        ......

     可以看见……就只是执行了super的方法就被return了。接下来就是FrameLayout#onMeasure方法了。由于FrameLayout#onMeasure方法内部逻辑比较复杂,这里就只贴一部分,其他的我都省略了。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int count = getChildCount();
    
            int maxHeight = 0;
            int maxWidth = 0;
            int childState = 0;
    
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if (mMeasureAllChildren || child.getVisibility() != GONE) {
                    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                    ……
                    childState = combineMeasuredStates(childState, child.getMeasuredState());
                    ……
                }
            }
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    resolveSizeAndState(maxHeight, heightMeasureSpec,
                            childState << MEASURED_HEIGHT_STATE_SHIFT));
            ……
        }

     这里会涉及到很多新概念新东西,我们尽量忽略一些和本篇无关的东西。这里在第七行引入了一个叫childState的变量,包括下面的resolveSizeAndState方法,这个涉及到一些新概念,这里就不详细说了,如果有读者想了解这一段,这篇博文解释了这里面的内容: 源码解析Android中View的measure量算过程。
     当子View的可视状态不为GONE时,调用了measureChildWithMargins,这个方法我们在前面提到了,这是测量子View的一个开端。然后下面就是调用了View#setMeasuredDimension方法,View#setMeasuredDimension这个方法定义在View类中,意味着一轮测量的结束,这个方法调用之后,就能通过getMeasuredHeightgetMeasuredWidth来获取宽高。(注意measureChildWithMargins是去测量子View,View#setMeasuredDimension则是测量View本身。)

     我们使用最简单的情况,目前已经已经强制将TabLayoutheightMeasureSpec置为了EXACTLY模式,所以如果我们的widthMeasureSpec为一个具体的值,情况就为简单很多,接下来的分析中,我们就假定TabLayout及其父类和祖先们的的宽度也为MATCH_PARENT,这样就也会直接解析成EXACTLY模式。

    我们继续往下看,越过了各种superonMeasure方法,现在我们的TabLayout有了measureWidthmeasureHeight,那么接下来就简单一点儿了。

             if (getChildCount() == 1) {
                final View child = getChildAt(0);
                boolean remeasure = false;
    
                switch (mMode) {
                    case MODE_SCROLLABLE:
                        remeasure = child.getMeasuredWidth() < getMeasuredWidth();
                        break;
                    case MODE_FIXED:
                        remeasure = child.getMeasuredWidth() != getMeasuredWidth();
                        break;
                }
    
                if (remeasure) {
                    int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
                            + getPaddingBottom(), child.getLayoutParams().height);
                    int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            getMeasuredWidth(), MeasureSpec.EXACTLY);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                }
            }

     这里的getChildCount()获取了如果只有一个子View(实际上就是TabLayout#SlidingTabStrip)的情况下(肯定只有一个View啊……),会有一个是否重新测量SlidingTabStrip的标记,这里都只对宽度做了动作,两种情况:

  • SCROLLABLESlidingTabStrip小于TabLayout的宽度
  • FIXEDSlidingTabStrip不等于TabLayout的宽度
  • 而重新测量子 SlidingTabStrip的内部,则将其宽度置成了和 TabLayout一样的宽度。这将在下面的代码中有所反映。

    TabLayout#SlidingTabStrip#onMeasure

     我们已经看完了TabLayoutonMeasure方法,接下来我们看看TabLayout的唯一一个子View,SlidingTabStrip的测量过程。

     因为TabLayout继承自HorizontalScrollView,故继承自LinearLayoutSlidingTabStrip就成为了唯一一个TabLayout内容区域的View。TabLayout在构造方法中加入了SlidingTabStrip类的实例。

    mTabStrip = new SlidingTabStrip(context);
            super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
                    LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));

     这里我们需要记住一件事情,就是LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT,这个将决定后面的测量结果。好,我们来看SlidingTabStriponMeasure方法。

     @Override
            protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                //第一段
                if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
                    // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
                    // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
                    return;
                }

     我们先来看第一段,这个地方逻辑倒是不麻烦,首先调用了super方法,因为SlidingTabStrip继承自LinearLayout,所以调用super方法应该是它的UI特性和LinearLayout相同,实际上最终的UI效果的确验证了这一点,接下来判断如果widthMeasureSpec的模式不是EXACTLY的情况下,直接return,里面的注释写着:HorizontalScrollViewTabLayout父类)在第一次测量过程中会将子View置为UNSPECIFIED,之后才为EXACTLY,所以忽略不为EXACTLY的情况。
    HorizontalScrollView因为重写了measureChildWithMargins方法,所以TabLayout在测量SlidingTabStrip的时候会调用HorizontalScrollView#measureChildWithMargins方法,里面有这样一句代码。

        final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),MeasureSpec.UNSPECIFIED);

     而MeasureSpec#makeSafeMeasureSpec的这个方法内部使得size被置为0,所以我们第一次运行到SlidingTabStrip#onMeasure的时候,sizemode分别是0和UNSPECIFIED,如果这样的数据最后传给了setMeasuredDimension方法,那么明显不符合最后的UI表现,所以我们这个时候回到上面对TabLayout#onMeasure最后一部分的分析,如果在SCROLLABLE模式下SlidingTabStripmeasureWidth小于TabLayout本身的measureWidth,或者在FIXED模式下不等于TabLayout本身的measureWidth,则将SlidingTabStrip直接置为TabLayout的宽度。

                //第二段
                if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
                    final int count = getChildCount();
    
                    int largestTabWidth = 0;
                    for (int i = 0, z = count; i < z; i++) {
                        View child = getChildAt(i);
                        if (child.getVisibility() == VISIBLE) {
                            largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
                        }
                    }
    
                    if (largestTabWidth <= 0) {
                        return;
                    }
    
                    final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
                    boolean remeasure = false;
    
                    if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
                        for (int i = 0; i < count; i++) {
                            final LinearLayout.LayoutParams lp =
                                    (LayoutParams) getChildAt(i).getLayoutParams();
                            if (lp.width != largestTabWidth || lp.weight != 0) {
                                lp.width = largestTabWidth;
                                lp.weight = 0;
                                remeasure = true;
                            }
                        }
                    } else {
                        mTabGravity = GRAVITY_FILL;
                        updateTabViews(false);
                        remeasure = true;
                    }
    
                    if (remeasure) {
                        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                    }
                }
            }

     我们再来看第二段,第二段的都在一个if条件中,只有在在mode和gravity分别在MODE_FIXEDGRAVITY_CENTER模式下才符合,我们这个时候回去看看那张图。这种模式下Tab呈现出几个特点。首先是Tab整个内容区域居中,然后每个Tab都是相同大小,大小的尺寸为最大的那个Tab的尺寸。在第二段的7到12行,取得了所有View中最大的那个View的尺寸。赋给了int变量largestTabWidth。然后在22行,做了一个最极端的情况,,以largestTabWidth乘上Tab的数量,与整个TabLayoutwidth-32dp相比较,如果是小于这个值的情况下,则表示以当前的Tab的情况,可以使用MODE_FIXED+GRAVITY_CENTER,则在25行的for循环内,将所有的Tab全部置为最大的那个Tab值。这样的做法符合上面图中的表现。

    值得注意的是,在取TabLayout的测量宽度的时候,减去了两个16dp,并将变量名取为gutter,这个做法就是区别一些情况下GRAVITY_FILLGRAVITY_CENTER的差别,GRAVITY_FILL两边是没有边距的,而GRAVITY_CENTER不管你掌握的如何好,和两边至少都有16dp的边距。这个在其他设备上还不一定为16dp,在谷歌Material Design网站中,存在这个的一些规范,而且他取的名字也叫gutter

     如果不符合上面那种情况呢?比如说有一个Tab特别大,然后其他的Tab又比较小,计算下来超过了TabLayout的内容宽度,这个时候就将mode置成GRAVITY_FILL,同时调用了updateTabViews(false);去刷新了View。

    我们回顾一下TabLayoutSlidingTabStrip的测量过程。

  • 1:如果开发者在定义TabLayout本身的高度时,选择了WRAP_CONTENT或者因为父View的WRAP_CONTENT导致在measure阶段使其模式置为非EXACTLY的模式(AT_MOST,UNSPECIFIED),在这种情况下,TabLayout都将高度指定为一个精确值,根据有icon和没有的情况,分别为72dp和48dp,mode置为EXACTLY
  • 2:对于TabLayout本身的宽度,并没有进行特别的处理,所遵从的规则基于FrameLayout,具体过程可参见FrameLayout#onMeasureView#resolveSizeAndState方法。
  • 3:SlidingTabStrip作为TabLayout的唯一一个子View,在TabLayout的构造方法中,宽和高分别指定为WRAP_CONTENTMATCH_PARENT
  • 4:SlidingTabStrip的高度为MATCH_PARENT,在TabLayout的高度size为精确值,mode为EXACTLY的情况下,应当与TabLayout本身的高度相同。
  • 5:SlidingTabStrip继承自LinearLayout,所遵从的规则基于LinearLayout,同时存在一个附加条件,因为Tab数量较少或者其他原因时,会根据当前两种mode的实际情况,在TabLayout#onMeasure方法中将SlidingTabStripwidthMeasureSpec的size置成和TabLayout相同宽度,mode置为EXACTLY
  • TabView

     前面的部分描述了TabLayoutSlidingTabStrip的测量内容。然后对于SlidingTabStrip这样一个继承自LinearLayoutViewGroup,囊括了所有的Tab,实际上,每一个Tab从View层级上来说,都是一个TabView,这个类继承自LinearLayout,下面我们就来详细看看这个类。
     在使用TabLayout的过程中,我们一般通过TabLayout#addTab方法,或者通过ViewPager,执行TabLayout#setupWithViewPager方法。无论是上面哪一种方式,最后都是通过TabLayout#addTabView方法来将一个Tab添加至TabLayout中。我们来看一下这个方法。

        private void addTabView(Tab tab) {
            final TabView tabView = tab.mView;
            mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
        }
    
        private LinearLayout.LayoutParams createLayoutParamsForTabs() {
            final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                    LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
            updateTabViewLayoutParams(lp);
            return lp;
        }
    
        private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
            if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
                lp.width = 0;
                lp.weight = 1;
            } else {
                lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
                lp.weight = 0;
            }
        }

     可以看见上面三个方法都是私有方法。我们先来看一下addTabView,在这个方法中,将取出了TabView,然后使用addView方法将TabView加入了SlidingTabStrip中。至于createLayoutParamsForTabsupdateTabViewLayoutParams方法,则是给TabView设置了LayoutParams,宽度设置为WRAP_CONTENT,高度设置为MATCH_PARENT。同时,只有在mode和gravity分别为MODE_FIXEDGRAVITY_FILL的情况下,将每一个TabView的权重设置为1,这也应证了前面那张图的UI表现。

        @Override
        public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) {
            final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
            final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
            final int maxWidth = getTabMaxWidth();
    
            final int widthMeasureSpec;
            final int heightMeasureSpec = origHeightMeasureSpec;
    
            if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED
                    || specWidthSize > maxWidth)) {
                // If we have a max width and a given spec which is either unspecified or
                // larger than the max width, update the width spec using the same mode
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST);
            } else {
                // Else, use the original width spec
                widthMeasureSpec = origWidthMeasureSpec;
            }
    
            // Now lets measure
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            ......
        }

     这是TabView#onMeasure方法。在这个方法里面,当指定的specWidthSize小于最大宽度时(默认为264dp)或者宽度的模式为UNSPECIFIED的情况下,将宽度置为最大宽度,模式置为AT_MOST。然后执行了super方法,再往后的逻辑都是关于TabView里面TextView的行数逻辑,因为不影响布局参数,我就没有贴代码了。所以,逻辑下发到TabView,只有app:tabMaxWidth属性对TabView产生了影响。这里上最后一张图,也算结束了这篇文章。这张图片是在mode=”scrollable”,gravity=”fill”下的情况。
    这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值