Android 中View的绘制机制源码分析 二

尊重原创:http://blog.csdn.net/yuanzeyao/article/details/46842891

本篇文章接着上篇文章的内容来继续讨论View的绘制机制,上篇文章中我们主要讲解了View的measure过程,今天我们就来学习ViewGroup的measure过程,由于ViewGroup只是一个抽象类,所以我们需要以一个具体的布局来分析measure过程,正如我上篇文章说的,我打算使用LinearLayout为例讲解measure过程,如果你还没有读过上篇文章,那么建议你先浏览一下上篇文章吧:Android中View的绘制机制源码分析 一

在进行今天的主题之前,我来给大家分享一下我最近看到并且非常喜欢的两句话吧:

1、把生命浪费在美好的事物上

2、应该有一份不以此为生的职业

喜欢第一句话的原因是因为里面包含了一种乐观的生活态度,只要一件事情你在进行的过程中能够给你带来快乐,那么我们就值得花时间做,喜欢第二句话的原因是作为程序员这个职业以后转型的问题也是值得我们考虑的,相信大家也都听说过程序员是吃青春饭的职业,当你到35-40岁已经年老色衰的时候,你不得不考虑转型了,有部分转型为管理人才,有些人完全转型,干着和IT毫无关系的职业,所以我们是不是现在就要想想我们有没有一份不以此为生的职业呢?好吧  扯淡就扯到这里吧,下面我们步入正题。

我们来分析今天的第一个问题:你对layout_weight属性知多少?

相信大多数同学会说这个属性就是标明一个View在父View中所占的权重(比例),这个解释对吗?我们暂且不做评论,我们使用两个例子来验证一下:

example 1:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
 >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="2"
        android:text="New Text"
        android:background="#998877"
        android:id="@+id/textView"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="4"
        android:text="New Text"
        android:background="#334455"
        android:id="@+id/textView2"
         />
</LinearLayout>

效果图如下:

example 2:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
 >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="2"
        android:text="New Text"
        android:background="#998877"
        android:id="@+id/textView"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="4"
        android:text="New Text"
        android:background="#334455"
        android:id="@+id/textView2"
         />
</LinearLayout>

效果图如下:

在第一张图片中,上面的TextView的weidht是2,下面的TextView的weight是4,所以上面的TextView的高度是下面TextView高度的一半,注意此时两个TextView的layout_height都是0dip,再看下面的一张图,同样上面的TextView和下面TextView的weight分别是2和4,唯一不同的是它们的layout_height变为了match_parent,此时上面的高度确实下面的两倍

所以从第一张图片看来,layout_weight好像是代表比例的,但是从第二张图片看,刚好是相反的,我们今天就带着这个疑问开始分析LinearLayout的measure源码吧

LinearLayout的measuer调用的是View中的measure方法,从上篇文章中我们知道measure会调用onMeasure方法,所以直接从LinearLayout的onMeasure开始分析:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

看了源码是不是觉得so easy!,在onMeasure主要根据当前的LinearLayout是横向还是纵向,分别调用measureVertical方法和measureHorizontal方法,这里我们以纵向为例,看看measureVertical代码,由于代码比较长,我们分段分析:

Section one:

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
		//用来存储所有的子View使用的高度
        mTotalLength = 0;
        int maxWidth = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
		//所有View的weight的和
        float totalWeight = 0;
		//获得子View的个数
        final int count = getVirtualChildCount();
        //widthMeasureSpec和heightMeasureSpec就是父View传递进来的,这里拿到父View的mode
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

这里定义了几个重要的变量,mTotalLength,用来存储所有子View的高度,count存在子View的个数,widthMode和heightMode用来存储父View的mode(如果对于mode不熟,我以看我前面的一篇文章)。

Section Two:

	//遍历所有的子View,获取所有子View的总高度,并对每个子View进行measure操作
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null) {
				//如果child 是Null,则mTotalLength加0
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
			   //如果child不可见,则跳过
               i += getChildrenSkipCount(child, i);
               continue;
            }
			//拿到child的LayoutParams
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
			//将weight的值加到totalWeight,weight的值就是xml文件中的layout_weight属性的值
            totalWeight += lp.weight;

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                /**
				  如果父View的mode是EXACTLY,并且height==0 并且lp.weight>0(就是我们上面的例子中的第一张图的情况)
				  那么就先不measure这个child,直接把topMargin和bottoMargin等属性加到totaoLength中
				*/
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            } else {
                int oldHeight = Integer.MIN_VALUE;
				//如果父View不是EXACLTY,那么将子View的height变为WRAP_CONTENT
                if (lp.height == 0 && lp.weight > 0) {
                    // heightMode is either UNSPECIFIED or AT_MOST, and this
                    // child wanted to stretch to fill available space.
                    // Translate that to WRAP_CONTENT so that it does not end up
                    // with a height of 0
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);

                if (oldHeight != Integer.MIN_VALUE) {
                   lp.height = oldHeight;
                }

                final int childHeight = child.getMeasuredHeight();
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

这段代码是整个measureVertical最核心的部分了,我已经在代码中加入了相应的注释,这里我只想说的是为什么child.getLayoutParam能够直接强制转换为LinearLayout.LayoutParams,这个问题我们先保留吧,我打算后面的文章中专门分析一下这个LayoutParams这个对象。

我们发现在measureVertical中调用了一个measureChildBeforeLayout方法,我们先看看它传入的几个参数,我们发现最后一个参数听奇怪的,totalWeight==0?mTotalLength:0,也就是说对于一个View,如果这个View之前的View没有设置过layout_weight属性,那么这个参数等于mTotalLength,如果有设置过,那么传0,我们先进入measureChildBeforeLayout方法看看:

   void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }

其实就是调用父类ViewGroup的measureChildWidthMargins方法,这个方法我们在前篇文章已经分析过了,这里我们就不分析了,它就是对子View进行measure方法,只不过我们这里需要注意,如果前面有View设置了layout_weight属性,那么这里的totalHeight就是0,在执行完了measureChildBeforeLayout方法后,child的高度就知道了,就将child的高度累加到mTotalHeight中。

Section Three:

//将所有View的高度赋值给heightSize;
        int heightSize = mTotalLength;

        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        //这里对heightSize再次赋值,不过如果LinearLayout是xml文件的根标签,并且设置到Activity的话
		//此时heightSize的大小就是屏幕的高度,我们暂时就考虑等于屏幕高度的情况,其他情况类似
        heightSize = resolveSize(heightSize, heightMeasureSpec);

		//屏幕的高度还剩下delta,如果对于我们上面第一张图,delta>0,对于第二张图则<0
        int delta = heightSize - mTotalLength;
        if (delta != 0 && totalWeight > 0.0f) {
			//如果设置了weightsum属性,这weightSum等于weightsum的属性,否则等于totalWeight
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;
			//重新遍历所有的子View
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                //如果子View不可见,直接跳过
                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

                float childExtra = lp.weight;
				//如果设置了weight属性
                if (childExtra > 0) {
                    // Child said it could absorb extra space -- give him his share
					//从delta中分到(weight/weightSum)*delta,注意这里delta可能<0
                    int share = (int) (childExtra * delta / weightSum);
                    weightSum -= childExtra;
                    delta -= share;

                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight +
                                    lp.leftMargin + lp.rightMargin, lp.width);

                    // TODO: Use a field like lp.isMeasured to figure out if this
                    // child has been previously measured
                    if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
						/**
						记得heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0吗
						这个是Section Two的一个判断条件,也就是说如果走到这里,说明这个View前面已经measure过
						现在要将share的值加入到高度上,所以要重新measure

						*/
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }

                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    } else {
                        /**
						由于走到Section Two中走到heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0时,是直接跳过的
						所以没有测量过,所以在这里对View进行测量
						*/
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));
                    }
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);
        }

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        //所有的孩子View测量完毕,为自己设置大小
        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);

这段代码主要是来重新绘制设置有layout_weight属性的子View,首先计算LinearLayout能够提供的高度大小heightSize,正如注释里面说的,在上面的两个例子中,heightSize都是屏幕的高度,然后通过heightSize和mTotalLenght计算还剩下的高度,然后将这些高度按照weight的比例分配给相应的View,然后调用View的measure方法,我们现在来解释上面的两个例子吧:

第一个例子:两个TextView的高度都是0dip,layout_weight分别是2 和 4,LinearLayout的mode=EXACTLY

从Section Two开始,条件满足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 所以在SectionTwo执行完后两个TextView是没有执行measure的,所以mTotalLenght等于0。

进入Section Three,此时heightSize等于屏幕的高度,所以delta=heightSize-mTotalLenght=屏幕高度。weightSum=2+4=6.在遍历子View的时候,通过计算第一个TextView的高度是:屏幕高度*(2/6),并且delta=delta-屏幕高度*(2/6).weightSum=6-2=4.

由于第一个TextView不满足条件(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY),所以执行else里面的逻辑:

child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));

所以第一个TextView的高度就是屏幕的1/3.

遍历完第一个TextView之后,遍历第二个TextView,同样的道理第二个 TextView的高度等于delta*(4/4),也就是等于delta的值,其实也就是 屏幕高度*(4/6)。

第二个例子:两个TextView的高度都是match_parent,layout_weight分别是2和4 ,LinearLayout的mode=EXACTLY

从Section Two开始,条件不满足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ,所以执行到了else里面的逻辑

measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);

此时totalWeight明显不等0,所以measureChildBeforeLayout最后一个参数明显是0,所以导致第一个View的高度绘制出来及时heightMeasureSpec的size,也就是屏幕的高度(原因见我上篇文章的分析)。同样的道理对于第二个TextView测量后高度也是整个屏幕的高度。所以导致这里算出的delta=(-屏幕的高度),也就是说是个负数,进入Section Three,很明显满足了(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)这个条件,所以执行如下代码:

int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }

                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));

通过Section Two的分析child.getMeasureHeight等于屏幕高度,share=-屏幕高度*(2/6),也就是说第一个TextView的高度变为 屏幕的高度*(4/6),同样的道理可以得出第二个TextView的高度 屏幕的高度*(2/6)。

对于layout_weight属性的理解应该是这样的:在SectionTwo中测量完所有的View后,将delta的值按照weight的比例给相应的 View,如果delta>0,那么那么就是在原来大小上加上相应的值,否则就是减去相应的值。

最后调用setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize) 设置自身的大小。

相信到这里你应该已经对LinearLayout的测量过程有了很深刻的理解了吧,如果还有觉得描述不清楚的地方,欢迎留言讨论...

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-23 15:20:29

Android 中View的绘制机制源码分析 二的相关文章

Android 中View的绘制机制源码分析 三

到目前为止,measure过程已经讲解完了,今天开始我们就来学习layout过程,不过在学习layout过程之前,大家有没有发现我换了编辑器,哈哈,终于下定决心从Html编辑器切换为markdown编辑器,这里之所以使用"下定决心"这个词,是因为毕竟Html编辑器使用好几年了,很多习惯都已经养成了,要改变多年的习惯确实不易,相信这也是还有很多人坚持使用Html编辑器的原因.这也反应了一个现象,当人对某一事物非常熟悉时,一旦出现了新的事物想取代老的事物时,人们都有一种抵触的情绪,做技术的

Android 中View的绘制机制源码分析 一

尊重原创: http://blog.csdn.net/yuanzeyao/article/details/46765113 差不多半年没有写博客了,一是因为工作比较忙,二是觉得没有什么内容值得写,三是因为自己越来越懒了吧,不过最近我对Android中View的绘制机制有了一些新的认识,所以想记录下来并分享给大家.在之后的几篇博客中,我会给大家分享如下的内容: 1.View中measure(),layout(),draw()函数执行过程分析,带领大家详细分析View的尺寸测量过程,位置计算,并最终

Android 中View的绘制机制源代码分析 三

到眼下为止,measure过程已经解说完了,今天開始我们就来学习layout过程.只是在学习layout过程之前.大家有没有发现我换了编辑器,哈哈.最终下定决心从Html编辑器切换为markdown编辑器.这里之所以使用"下定决心"这个词.是由于毕竟Html编辑器使用好几年了.非常多习惯都已经养成了,要改变多年的习惯确实不易.相信这也是还有非常多人坚持使用Html编辑器的原因. 这也反应了一个现象.当人对某一事物非常熟悉时,一旦出现了新的事物想代替老的事物时,人们都有一种抵触的情绪,做

Android中AsyncTask基本用法与源码分析(API 23)

原文链接 http://sparkyuan.github.io/2016/03/23/AsyncTask源码剖析(API 23)/ 转载请注明出处 Android的UI是线程不安全的,想在子线程中更新UI就必须使用Android的异步操作机制,直接在主线程中更新UI会导致程序崩溃. Android的异步操作主要有两种,AsyncTask和Handler.AsyncTask是一个轻量的异步类,简单.可控.本文主要结合API 23的源码讲解一下AsyncTask到底是什么. 基本用法 声明:Andr

【Flume】flume中FailoverSinkProcessor容错处理机制源码分析

FailoverSinkProcessor顾名思义是flume中sink输出容错的处理器 继承自AbstractSinkProcessor 先看下整体源码 /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional i

Android View 事件分发机制源码详解(View篇)

前言 在Android View 事件分发机制源码详解(ViewGroup篇)一文中,主要对ViewGroup#dispatchTouchEvent的源码做了相应的解析,其中说到在ViewGroup把事件传递给子View的时候,会调用子View的dispatchTouchEvent,这时分两种情况,如果子View也是一个ViewGroup那么再执行同样的流程继续把事件分发下去,即调用ViewGroup#dispatchTouchEvent:如果子View只是单纯的一个View,那么调用的是Vie

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

1 背景 还记得前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制. PS:阅读本篇前建议先查看前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>,这一篇承接上一篇. 关于View与ViewGroup的区别在前一篇的A

Android -- 消息处理机制源码分析(Looper,Handler,Message)

android的消息处理有三个核心类:Looper,Handler和Message.其实还有一个Message Queue(消息队列),但是MQ被封装到Looper里面了,我们不会直接与MQ打交道,因此我没将其作为核心类.下面一一介绍: Looper Looper的字面意思是“循环者”,它被设计用来使一个普通线程变成Looper线程.所谓Looper线程就是循环工作的线程.在程序开发中(尤其是GUI开发中),我们经常会需要一个线程不断循环,一旦有新任务则执行,执行完继续等待下一个任务,这就是Lo

Android中View的绘制过程 onMeasure方法简述

Android中View的绘制过程 当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点. 绘制过程从布局的根节点开始,从根节点开始测量和绘制整个layout tree. 每一个ViewGroup 负责要求它的每一个孩子被绘制,每一个View负责绘制自己. 因为整个树是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制. 绘制是一个两遍(two pass)的过程:一个mea