Android View绘制之旅

1.说在起点的话

很早前就想将View绘制原理这块给搞清楚搞透彻,但是奈何自己无知还是愚钝,总未能得真经,所以此次决意好好出发,做到有始有终。
我分析了一下自己的问题,自己实在太功利了,总希望看一两篇博文就能够把这个概念给理解透彻,“欲速则不达”,回忆我以前的经历,在学校一学期没学懂的东西,在公司实践不到一个星期就很明白了,这是为什么?缺乏思考,缺乏从追根溯源的思考,哪里来的体会,哪里来的真知呢?你不让小孩们去忆父辈的苦那会对今日的甜有所感悟和珍惜?

2.从起点出发

现在你就是那位Android系统的总设计师,面临的是一穷二白,所有代码为0,只有需求,你要做出一个界面展示系统。面临着很多问题:

总结一下问题就是:

在哪里画 ——位置
画多大 ——大小
画什么 —— 内容

xml语言!

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:id="@+id/ll_sec"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tv_three"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="Hello world" />

        <Button
            android:id="@+id/btn_three"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="I am a button" />
    </LinearLayout>

    <TextView
        android:id="@+id/tv_sec"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello world" />

</LinearLayout>

2.1.猜及源码分析

根线性布局里又包含个几个子View,某个子布局中又包含子View
父子关系?也被称为树结构

宽高测量流程 猜测

  • ll_root
    这个页面的老大,它的height和width都是match_parent,毫无疑问,它的地盘是整个页面。它地盘上有两个小弟 ll_sec,tv_sec
  • ll_sec
    有自己的小弟,不过它还不知道自己要占多大地儿,得做一下普查。
    这里写图片描述
    这一流程下来 View的大小算是有着落了。

宽高测量流程 源码

为了承接上文,就以ll_sec为例说起:

void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
/*中间代码省略*/
final int count = getVirtualChildCount();
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);
    /*中间代码省略*/
    measureChildBeforeLayout(child, i, widthMeasureSpec,
                        totalWeight == 0 ? mTotalLength : 0,
                        heightMeasureSpec, 0);
   final int childWidth = child.getMeasuredWidth();
                if (isExactly) {
                    mTotalLength += childWidth + lp.leftMargin + lp.rightMargin +
                            getNextLocationOffset(child);
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin +
                           lp.rightMargin + getNextLocationOffset(child));
                }
    int widthSize = mTotalLength;

    // Check against our minimum width
    widthSize = Math.max(widthSize, getSuggestedMinimumWidth());
    /*中间代码省略*/
    // Reconcile our calculated size with the widthMeasureSpec
    int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
    widthSize = widthSizeAndState & MEASURED_SIZE_MASK;
    /*中间代码省略*/
    setMeasuredDimension(widthSizeAndState | (childState&MEASURED_STATE_MASK),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        (childState<<MEASURED_HEIGHT_STATE_SHIFT)));
}

上面的代码只是敢重点的挑,主要是宽度的计算,可以看出其基本上就是将横向的所有child View 的宽度相加。

然后再看看高度呢?这里将是更精简的代码摘录

final int childHeight = child.getMeasuredHeight() + margin;
maxHeight = Math.max(maxHeight, childHeight);   
/*.....*/
maxHeight = Math.max(maxHeight, childHeight);
/*.....*/
// Check against our minimum height
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());

        setMeasuredDimension(widthSizeAndState | (childState&MEASURED_STATE_MASK),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        (childState<<MEASURED_HEIGHT_STATE_SHIFT)));

跟我们的猜测也是基本吻合的,取所有Child View的最大高度。

Child View之Measure 玄机

像上面的例子中,ll_sec是WRAP_CONTENT,那么它就要知道它的Child View的高或宽是多少,这样才能算出自己的高宽。
在Linearlayout中其执行的函数顺序是:
measureChildBeforeLayout –> measureChildWithMargins –> getChildMeasureSpec –> child.measure

最终,其调用的是这个方法。
View.measure(int widthMeasureSpec, int heightMeasureSpec)

This is called to find out how big a view should be. The parent supplies constraint information in the width and height parameters.
The actual measurement work of a view is performed in onMeasure(int, int), called by this method. Therefore, only onMeasure(int, int) can and must be overridden by subclasses.

Parameters
widthMeasureSpec
Horizontal space requirements as imposed by the parent
heightMeasureSpec
Vertical space requirements as imposed by the parent

这段官方的说明我一直看的不太明白。ViewGroup的getChildMeasureSpec函数就是产生上面两个参数的逻辑,只要能理解他们就能通晓

MeasureSpecs三模式

测量规格,包含测量要求和尺寸的信息,有三种模式:
UNSPECIFIED
父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如ListView、ScrollView,一般自定义View中用不到.
EXACTLY
父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体指,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。
AT_MOST
父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 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
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

通过这些函数可以了解到,作为父容器的ll_sec来说,根据其自身的spec配置以及child View 的xml中的配置(MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams()),
一部分情况其是可以预知到child View的高或宽来着,如父容器是MATCH_PARENT,也就是EXACT,而其child View对应量为固定值30dp,那么这样child View的量基本上以及确认了,就是30dp + EXACT,就是说,你不用测了,你我的配置都已经写的很明白了。
但另一部分情况却无法事先了解,但是可以给child View一些限制:如父容器是WRAP_CONTENT,而其child View为MATCH_PARENT,这个父容器只能给其child View 一个限定值 + AT_MOST.(这个限定值就是当前父容器还剩余的最大未使用空间),那么意思就是说,测量宽高你说了算,不过是有个限度的。

布局位置测量流程 猜测

坐标系

这个需要一个坐标系,我们上学时候标准的坐标系是这样的。
这里写图片描述
很显然,如果坐标原点放在左下角,有一点共识是,我们开始去计算 View的位置时候,是希望从(0,0)开始的,那么这意味着在写界面的时候,我们得从左下角开始,这很不符合我们的习惯,就像写字看书一样,现实中,我们都是从左上角开始,哈哈,看来写代码之View绘制也是一样啊。

最后下图的倒着的坐标系就诞生了。
这里写图片描述

P.s :这么简单的道理之前一直不懂,实践是多么的重要啊。

布局位置

ll_root毫无疑问是从(0,0)开始,作为ll_root 的站第一的小弟ll_sec 也是(0,0) ,那么站第二的小弟tv_sec的位置则是(0,height of (ll_sec));

ll_sec 里面又有两个嵌套的小弟,这两个小弟需要使用一个以ll_sec左上角为原点的相对坐标系,对于他们来说只有ll_sec罩着它,布局的计算过程和外层的是一样的,但如果想知道它在外层坐标系的布局位置也很简单,将ll_sec的在外层坐标系x,y的值加上自身在相对坐标系的x,y值就可以了

布局位置测量流程 源码

在上面的Measure过程结束后,也是从View树的根部开始逐级的为每个View安排布局位置,父容器有责任将其child的所有的布局位置给计算出来,这里不会存在说某个child的布局位置还不确定的情况。

void layoutHorizontal(int left, int top, int right, int bottom)
    /*..省略代码..*/
    for (int i = 0; i < count; i++) {
            int childIndex = start + dir * i;
            final View child = getVirtualChildAt(childIndex);
            /*.....*/
            childLeft += lp.leftMargin;
            /*设置child view的左右顶底的位置*/
            setChildFrame(child, childLeft + getLocationOffset(child), childTop,childWidth, childHeight);
            /*childLeft会一直累计*/
            childLeft += childWidth + lp.rightMargin +
                        getNextLocationOffset(child);

跟猜测基本一致的。
这里需要理解:
childLeft:就是当前的child的左边距其父容器左边界的长度。因为这是线性布局的横向布局,所以在横向上面需要累积。同理还有childTop.
每个View的mleft, mtop, mright, mbottom 位置都是相对其父容器的,并不是绝对位置。
这里写图片描述

绘制过程 猜测

就把这个屏幕抽象成一张白纸,经过前面的过程,我们在这张白纸上基本确认大体的框架,比如在哪个位置上是开始绘制某某组件,它占多大的面积,那么现在该是着笔的时候了。
大家其实都明白,其实显示屏都是由一个个的小小的像素点组成,每个像素点可以显示很多种颜色,它显示什么颜色由显存中的几个字节来控制,显存就是一个普通的存储器,显存的内容以极快的速度被更新,这样显示屏就会随之显示了不同的画面。

View树也是逐级从根部开始绘制

这里写图片描述

View树中的某个view如何找到自己在白纸上落笔的位置,

ll_sec 拿到白纸,利用和父容器的相对位置,将白纸trans到自己应该的位置上, 然后将白纸交给tv_three,tv_three再利用和ll_sec的相对位置,将白纸trans到自己应该的位置上,开始draw,结束后,将白纸的位置恢复到ll_sec传入的初始状态,接着 btn_three拿到白纸,继续上面的过程。

理解:canvas.save() …. canvas.restore()出现的来由是什么?

跟我们实际生活绘制不太一样,实际生活中,当我们希望在不同位置绘制的时候,我们会移动我们的画笔到画布的某个位置上绘画。而在这里,我们的画笔不动,改变的是我们的画布位置,当我们想在当前位置的上方绘画,那么需要将画布下拉一段距离。
这个view树的绘制 都是共用同一个canvas,假如在View A中绘制的时候我调用canvas.translation(),那么这将导致其它跟View A无关的View在一个已经translate的位置上绘制,这导致绘制出错。
正确的方式是:在View A 绘制前 代用canvas.save(),这个时候View A根据自己的需要trans到自己想要的位置上绘制,结束后,调用canvas.restore.
再用上面的比方解释就是,其实不光是一个人而是有很多不同的人在这张画布上绘画,当我移动画布在我想要的位置上画完后,这个时候你应该再将画布的位置还原,否则其他共用这张画布的人就会在错误的位置上绘画了。

XML是由android系统谁来解析出来的?

大名鼎鼎的LayoutInflater.inflate。利用XmlResourceParser来进行Xml的解析。

上面的ll_root的幕后老大又是谁?

在PhoneWindow.setContentView方法中有如下:

/*This is the view in which the window contents are placed. It is either mDecor itself, or a child of mDecor where the contents go.*/
private ViewGroup mContentParent;

mLayoutInflater.inflate(layoutResID, mContentParent);

ll_root最后会塞给mContentParent;
而mContentParent 又会塞给DecorView

末了,了解这些,我们又能做些什么?

自定义View?
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值