美文网首页
Android View的绘制简单分析二

Android View的绘制简单分析二

作者: 梧叶已秋声 | 来源:发表于2020-07-11 16:38 被阅读0次

    前面简单分析了View的绘制流程,接下来会具体分析下绘制过程中ViewGroup以及其下的View是如何计算测量,布局这些数据的,以及View如何绘制到界面上。
    下面以FrameLayout为例去进行具体了解。FrameLayout是继承自ViewGroup的。ViewGroup是继承自View的。

    用到了设计模式中的模板方法模式,模板方法模式是一种行为型模式。适用于对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即一次性地实现一个算法的不变部分,并将可变的行为留给子类来实现。


    设计模式的艺术软件开发人员内功修炼之道

    ViewGroup的测量大小就是一种复杂的算法,需要考虑多种因素,例如子View的诉求或父容器的大小。套用模板方法的话结构如下:

    1.png
    public class View {
        public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            ..............
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            .....................
        }
    
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           .............
        }
    
    }
    
    
    public abstract class ViewGroup extends View {}
    
    public class FrameLayout extends ViewGroup {
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         ............
        }
    
    }
    

    通过在FrameLayout这种ViewGroup中重写onMeasure等onXXX函数,从而定义不同的容器控件,例如LinearLayout、RelativeLayout 等,以完成不同的布局需求。

    测量的本质就是根据约束条件去做计算,计算出控件的大小。

    那么FrameLayoutonMeasure中是如何计算出了FrameLayout以及FrameLayout下的子View的大小,但是具体到底是如何计算的呢?下面具体来看一看。

    以下代码均为由源码改的不完全代码。

    // FrameLayout.java
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    ............
            final boolean measureMatchParentChildren =
                    MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                    MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    ............
    }
    
    

    widthMeasureSpecheightMeasureSpec代表FrameLayout的父容器对FrameLayout的要求。所以下面的代码意思是
    如果父容器LinearLayoutFrameLayout没有一个具体的大小要求,那么就要测量FrameLayout下的所有的长或高设置为MATCH_PARENT的子View。可以想象成下面的格式。

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="AB"/>
        </FrameLayout>
    
    </LinearLayout>
    

    FrameLayout的父容器大小设置为wrap_content,那么对FrameLayout的大小要求就不是一个确定的值,那么就需要对FrameLayout下所有设置为match_parent的子View例如TextView的大小进行二次测量。至于为什么要对TextView二次测量,具体原因往下看就知道了。接着往下看,下面这一段是遍历测量添加到framelayout下的子view

    //FrameLayout
    //这段是重点关注部分
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if (mMeasureAllChildren || child.getVisibility() != GONE) {
                    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    maxWidth = Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                    maxHeight = Math.max(maxHeight,
                            child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                    childState = combineMeasuredStates(childState, child.getMeasuredState());
                    if (measureMatchParentChildren) {
                        if (lp.width == LayoutParams.MATCH_PARENT ||
                                lp.height == LayoutParams.MATCH_PARENT) {
                            mMatchParentChildren.add(child);
                        }
                    }
                }
            }
    

    为了具体看看这部分的代码可以先假定一个具体的xml文件。如下所示:

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:orientation="vertical">
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        <TextView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:text="AB"/>
        </FrameLayout>
    
    </LinearLayout>
    

    先来看这一句measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
    首先要对widthMeasureSpec有一个明确的认知。
    widthMeasureSpecframelayout的父容器(即LinearLayout)对framelayout的要求, LinearLayout大小设置为100x100,但是framelayout的大小设置为wrap_content
    表示LinearLayout要求framelayoutsize最多为 100,因此widthMeasureSpec中的modeAT_MOSTsize为100,widthMeasureSpec-2147483548(通过调用makeMeasureSpec(100,MeasureSpec.AT_MOST)计算而来),(widthMeasureSpec这个参数的计算这一部分实际是ViewRootImpl里面做的,实际是ViewRootImpl中的performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);ViewRootImpl中的childWidthMeasureSpec就是这里的widthMeasureSpec。但是这里先将这一部分模糊化,减少代码量,因为ViewRootImpl那里也有很多需要分析的部分),但是此时framelayout大小未定,framelayout的大小可能是10x10,也有可能是100x100,仍需继续向下测量framelayout下的view的大小,这里是一个TextView。那么TextView大小在定义了具体值的情况下如何测量。framelayoutTextView又有什么要求(即childWidthMeasureSpec)?
    下面来看看measureChildWithMargins具体做了什么。

    //FrameLayout
        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);
        }
    

    其实看这部分代码的时候,我一度迷失在widthMeasureSpec
    parentWidthMeasureSpec以及childWidthMeasureSpec这三个参数中,后来解决办法就是将这些参数具体化,给予它们具体是数值。

    首先,MeasureSpec表示父容器对子View的要求,这里的表示LinearLayoutFrameLayout的要求。然后parentchild都是站在FrameLayout的角度去考虑的。
    widthMeasureSpec的侧重点是width的MeasureSpec,即LinearLayoutFrameLayout的宽度要求,存储了modesize
    parentWidthMeasureSpec表示LinearLayoutFrameLayout的宽度要求,存储了modesize
    childWidthMeasureSpec表示FrameLayoutTextView的宽度要求,存储了modesize
    modesize的获取是通过getModegetSize

    Log.d(TAG, "getMode = " + MeasureSpec.getMode(mHeightMeasureSpec) + " ,getSize = " + MeasureSpec.getSize(mHeightMeasureSpec));
    

    这里先把xml文件修改一下。
    假设TextViewmPaddingLeft设置为10,没有设置margin参数,widthheight参数设置为20。

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:orientation="vertical">
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        <TextView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:paddingLeft="10dp"
            android:text="AB"/>
        </FrameLayout>
    
    </LinearLayout>
    
    

    来看childWidthMeasureSpec 的具体计算。
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);

    getChildMeasureSpec是将TextViewPaddingMargin等参数传递到measureChildWithMargins函数中,计算framelayout这个容器对TextView的要求,命名为childWidthMeasureSpec

    前面我们计算出了widthMeasureSpec为-2147483548,因此getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width)实际调用的是
    getChildMeasureSpec(-2147483548,10,20)

    //FrameLayout
    
        //getChildMeasureSpec(-2147483548,10,20)
    //-2147483548表示Mode 为 AT_MOST,size为100
        //specMode 为 AT_MOST ,childDimension >0 所以
        //resultSize = 20
        // resultMode = MeasureSpec.EXACTLY;
         // MeasureSpec.makeMeasureSpec(20, MeasureSpec.EXACTLY)  结果为  1073741844
        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 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;
    
              ......
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    
    

    也就是说 framelayout要求TextView的size值为20, modeEXACTLY,所以childWidthMeasureSpec = 1073741844(这个数据是调用makeMeasureSpec自行计算得出)。
    接下来就是child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    childWidthMeasureSpec 值为 1073741844,然后调用child.measure(1073741844, 1073741844);即调用TextView.measure(1073741844, 1073741844);
    1073741844表示framelayout要求TextView大小为20, modeEXACTLY

    measureChildWithMargins这一函数在此时就确定了没有MatchParent的情况下child的大小(即某些设定下的TextView 的大小)。

    前面是TextView size设置为固定值20dp的情况。
    如果TextView 大小属性设置为wrapcontent,并且TextView 没有设置LayoutParams情况下,childDimension默认是-1(因为LayoutParams.width默认是-1),此时int size = Math.max(0, specSize - padding); resultSize 则为100 - 10 = 90

    这些流程都可以通过在framelayout中添加log显示出来。自己测试一遍看log是最直观的理解方式。

    那么就调用MeasureSpec.makeMeasureSpec(90, MeasureSpec.AT_MOST),然后TextView 的测量会比较复杂。例如如果字符多一点就长一点少点就短点。
    最后width在经过一系列复杂的计算后,跟90比取小。

    //TextView.java
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...........
         if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(widthSize, width);
         }
         .............
         setMeasuredDimension(width, height);
    }
    

    如果TextView 大小属性设置为MatchParent,那么就添加进mMatchParentChildren中。后面还要用到。

    接着往下看。调用了getPaddingLeftWithForeground
    这里要明白Foreground以及padding的概念。
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();

    下面以xml文件为例说明。

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:orientation="vertical"
        tools:context="com.android.test.myapplication.MainActivity">
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">
        <TextView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:text="AB"/>
        </FrameLayout>
    
    </LinearLayout>
    
    image.png

    FrameLayoutpadding区域就是FrameLayout以内,TextView之外的区域。
    我把FrameLayoutpadding区涂成红色,如下图所示。这里我只设置了左右padding,没有设置上下padding。

    image.png

    下面来看Foreground

    出处:https://helpx.adobe.com/cn/photoshop/key-concepts/background.html
    背景(在美术和摄影中)图像的元素可能包含前景 (A) 和背景 (B)。背景是图像中离查看者最远的部分。

    image.png

    在上图中B(可能是地毯或者瓷砖?)是图像中离查看者最远的部分,是背景,最上面的即离查看者最近的是A(猫),是前景。A和B中间还有垫子之类的东西,这种就是中景

    前景是相对于背景而言的。
    使用画图工具,绘制一张大小为50x50像素的图片,填充色为粉色。这张作为前景图。

    test.png

    然后填充另一个颜色,将另存为background.png,这张作为背景图。


    background.png

    下面来简单对比设置background,foreground时View的显示。
    1.设置background

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:orientation="vertical">
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:background="@drawable/background">
        <TextView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:text="AB"/>
        </FrameLayout>
    
    </LinearLayout>
    

    可以看到background在TextView下面。


    image.png

    2.设置foreground

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:orientation="vertical">
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:foreground="@drawable/test">
        <TextView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:text="AB"/>
        </FrameLayout>
    
    </LinearLayout>
    

    此时看不到中景TextView,因为foreground把TextView挡住了。


    image.png

    3.同时设置foreground和background。

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:orientation="vertical">
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:foreground="@drawable/test"
            android:background="@drawable/background">
        <TextView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:text="AB"/>
        </FrameLayout>
    
    </LinearLayout>
    

    Framelayout设置了背景和前景,TextView是中景。
    但是由于前景图片大小和背景大小一致,所以只能看到前景图,并且中景TextView被挡住。


    image.png

    再来看

    //FrameLayout.java
    int getPaddingRightWithForeground() {
            return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
                mPaddingRight + mForegroundPaddingRight;
        }
    

    由于前景图大小超出了android:paddingRight设置的区域,因此调用mPaddingRight + mForegroundPaddingRight ,计算padding,算最大值。

    image.png

    再往下就是根据背景图和前景图大小,再次计算framelayout这个ViewGroup的最大值。

    //FrameLayout.java
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    ....................
            // Check against our minimum height and width
            maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
            maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    
            // Check against our foreground's minimum height and width
            final Drawable drawable = getForeground();
            if (drawable != null) {
                maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
                maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
            }
       setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    resolveSizeAndState(maxHeight, heightMeasureSpec,
                            childState << MEASURED_HEIGHT_STATE_SHIFT));
    ....................
    }
    

    最后通过调用setMeasuredDimension将测量所得的framelayout的大小进行存储。

    要捋清楚这个3个函数:measureonMeasuresetMeasuredDimension的作用。

    framelayout的大小与TextView有一定的关系,所以是测量完TextView子View的尺寸后,再加上padding等数据后所得的数据才是maxWidth
    最后存储的framelayout的测量数据,是比较了maxWidthwidthMeasureSpecchildState这3个数据后得出的数据。
    因此,如果TextView设置为matchparent,由于前景图片导致framelayout大小发生变化,那么此时childWidthMeasureSpec计算方法就会发生改变,要重新传递参数。

    framelayout为例的ViewGrouponMeasure的测量大小的计算过程基本就是这个样子了,在ViewGroup大小为给定具体数值的情况下,ViewGroup首先遍历测量子View的大小,然后再计算出自身的大小,再重新确定部分特殊的子View的大小。

    现实生活中的绘制,首先你需要一张纸,一支笔,以及打算画的对象(例如一棵树,以及树上的叶子)。这里FrameLayout和TextView的关系就类似于树与叶子的关系。纸可以看做一个ViewRootImpl对象(ViewRootImpl的理解部分可以简单看看这篇ViewRootImpl源码分析事件分发)。
    纸的大小是确定的,但是树和叶子要画多大呢?树和叶子的大小这个是需要由人来决定的。但是无论如何,树和叶子的大小都不可能超出纸的大小。
    编程的本质是和计算机沟通,将人的想法告诉计算机。
    为了确定树和叶子具体大小,首先你需要把这个你需要对树进行约束(例如要求不能超过纸的大小的二分之一又或者是不能超过整张纸的大小),告诉它们绝对不能超过这个大小,并且你需要定义树和叶子的大小,例如具体数值20px,或者match,又或者说wrap,并且,在定义了这些约束条件的情况下,才能测量树的大小。

    总结可以看这一段话。

    出处:[Android 自定义 View] —— 深入总结 onMeasure、 onLayout
    从个体看
    对于每一个 View:
    1.运行前,开发者会根据自己的需求在 xml 文件中写下对于 View 大小的期望值
    2.在运行的时候,父 View 会在 onMeaure()中,根据开发者在 xml 中写的对子 View 的要求, 和自身的实际可用空间,得出对于子 View 的具体尺寸要求
    3.子 View 在自己的 onMeasure中,根据 xml 中指定的期望值和自身特点(指 View 的定义者在onMeasrue中的声明)算出自己的*期望
    如果是 ViewGroup 还会在 onMeasure 中,调用每个子 View 的 measure () 进行测量.
    1.父 View 在子 View 计算出期望尺寸后,得出⼦ View 的实际尺⼨和位置
    2.⼦ View 在自己的 layout() ⽅法中将父 View 传进来的实际尺寸和位置保存
    如果是 ViewGroup,还会在 onLayout() ⾥调用每个字 View 的 layout() 把它们的尺寸 置传给它们

    下一篇来看看layout。其实测量与布局不能孤立地去看,要联系在一起。前面的设置前景图片的时候,影响了framelayout的最大值的计算,但是前景图的位置其实也是一个因素,如果前景图改变了位置,framelayout大小又会发生改变,只是这里暂时忽略了布局这个变量。

    参考链接:
    Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一)
    Android View 全解析(二) -- OnMeasure
    Android -容器- FrameLayout
    Android学习笔记---深入理解View#03

    背景

    参考书籍:
    《深入理解Android内核设计思想》
    《深入理解ANDROID 卷3》

    相关文章

      网友评论

          本文标题:Android View的绘制简单分析二

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