四、界面编程(二) 深入解析View的绘制流程

本文将详细介绍Android中View的绘制流程,侧重点在于对整体流程的分析,特定细节可以再查看相应的源码去学习理解。

1.从Activity到关联的Window

1.1 PhoneWindow介绍

  PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现。我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。

1.2 setContentView()介绍

  setContentView这个方法实际上是完成了Activity的ContentView的创建,并没有执行View的绘制流程。当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:

public void setContentView(@LayoutRes int layoutResID) {
   getWindow().setContentView(layoutResID);
   . . .
}

  getWindow()方法会返回Activity所关联的PhoneWindow,也就是说,实际上调用到了PhoneWindow的setContentView()方法,源码如下:

 @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
         // mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
         // 具有FEATURE_CONTENT_TRANSITIONS特性表示开启了Transition
         // mContentParent不为null,则移除decorView的所有子View
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // 开启了Transition,做相应的处理,我们不讨论这种情况
        // 感兴趣的同学可以参考源码
        . . .
        } else {
        // 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局
        // 填充布局也就是把我们设置的ContentView加入到mContentParent中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        . . .
        // cb即为该Window所关联的Activity
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
        // 调用onContentChanged()回调方法通知Activity窗口内容发生了改变
            cb.onContentChanged();
        }

       . . .
    }

1.3 LayoutInflater.inflate( )详解

  在上面我们看到了,PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        . . .
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

  在PhoneWindow的setContentView()方法中传入了decorView作为LayoutInflater.inflate()的root参数,我们可以看到,通过层层调用,最终调用的是inflate(XmlPullParser, ViewGroup, boolean)方法来填充布局。这个方法的源码如下:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
                . . .
            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;

            View result = root;

            try {
                // Look for the root node.
                int type;
                // 一直读取xml文件,直到遇到开始标记
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                // Empty
                }
                // 最先遇到的不是开始标记,报错
                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                . . .
                // 单独处理<merge>标签,不熟悉的同学请参考官方文档的说明
                if (TAG_MERGE.equals(name)) {
                // 若包含<merge>标签,父容器(即root参数)不可为空且attachRoot须为true,否则报错
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                // 递归地填充布局
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                // temp为xml布局文件的根View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                . . .
                // 获取父容器的布局参数(LayoutParams)
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                // 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View
                            temp.setLayoutParams(params);
                        }

                    }

                // 递归加载根View的所有子View
                    rInflateChildren(parser, temp, attrs, true);
                . . .

                    if (root != null && attachToRoot) {
                // 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来
                        root.addView(temp, params);
                    }

                // 若root为空或是attachToRoot为false,则以根View作为返回值
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                . . .
            } catch (Exception e) {
                . . .
            } finally {

                . . .
            }
            return result;
        }
    }

在上面的源码中,首先对于布局文件中的标签进行单独处理,调用rInflate()方法来递归填充布局。这个方法的源码如下:

void rInflate(XmlPullParser parser, View parent, Context context,
                  AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        // 获取当前标记的深度,根标记的深度为0
        final int depth = parser.getDepth();
        int type;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
             // 不是开始标记则继续下一次迭代
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            // 对一些特殊标记做单独处理
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                // 对<include>做处理
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                // 对一般标记的处理
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
                // 递归地加载子View
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

我们可以看到,上面的inflate()和rInflate()方法中都调用了rInflateChildren()方法,这个方法的源码如下:

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

  从源码中我们可以知道,rInflateChildren()方法实际上调用了rInflate()方法。
  到这里,setContentView()的整体执行流程我们就分析完了,至此我们已经完成了Activity的ContentView的创建与设置工作。接下来,我们开始进入正题,分析View的绘制流程。

2. View的绘制简介

  View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。
  当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

 @Override
 public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            // 检查发起布局请求的线程是否为主线程
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

  上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

  View的绘制过程只要是指measure、layout、draw这三大流程,也就是测量、布局和绘制,他们各自的实现的功能如下:

measure: 确定View的测量宽/高;
layout: 确定View的最终宽/高和四个顶点的位置;
draw: 绘制View到屏幕上。

3. View的绘制过程

3.1 准备知识

在了解measure 过程前,我们需要先了解measure过程中传递尺寸(宽 / 高测量值)的两个类:

ViewGroup.LayoutParams (View 自身的布局参数)
MeasureSpecs 类(父视图对子视图的测量要求)

3.1.1 ViewGroup.LayoutParams

这个类我们很常见,用来指定视图的高度(height)和宽度(width)等布局参数,可以通过以下参数进行指定:

android:layout_weight="wrap_content" //自适应大小
android:layout_weight="match_parent" //与父视图等高
android:layout_weight="fill_parent" //与父视图等高
android:layout_weight="100dip" //精确设置高度值为 100dip

  ViewGroup的子类有其对应的ViewGroup.LayoutParams子类;ViewGroup的子类包括RelativeLayout、LinearLayout等;例如RelativeLayout的ViewGroup.LayoutParams的子类就是RelativeLayoutParams。

3.1.2 MeasureSpec简介

  MeasureSpec类,主要作用是通过宽测量值(widthMeasureSpec)和高测量值(heightMeasureSpec)来决定View的大小。MeasureSpec是一个32位的int值,其中高2位为测量模式,低30位为测量的大小,构成如下图:
这里写图片描述
  其中,Mode模式共分为三类:EXACTLY(即精确模式),AT_MOST(即最大值模式),UNSPECIFIED(不指定其大小测量模式),具体说明如下图:
这里写图片描述

3.1.3 MeasureSpec类的使用

  MeasureSpec、Mode和Size都封装在View类中的一个内部类里——MeasureSpec类。
  MeasureSpec类通过二进制,将mode和size打包成一个int值来减少对象内存分配,用一个变量携带两个数据(size,mode),并提供了打包和解包的方法。具体源码如下:

public class MeasureSpec {
        //进位大小为2的30次方
        //int的大小为32位,所以进位30位就是要使用int的32和31位做标志位)
        private static final int MODE_SHIFT = 30;

        // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
        // 遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0
        private static final int MODE_MASK = 0x3 << MODE_SHIFT;

        // 0向左进位30 = 00后跟30个0,即00 00000000000
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        // 1向左进位30 = 01后跟30个0 ,即01 00000000000
        public static final int EXACTLY = 1 << MODE_SHIFT;

        // 2向左进位30 = 10后跟30个0,即10 00000000000
        public static final int AT_MOST = 2 << MODE_SHIFT;

        /* 根据提供的size和mode得到一个详细的测量结果 */
        public static int makeMeasureSpec(int size, int mode) {
            // measureSpec = size + mode
            //注:二进制的加法,不是十进制的加法!
            return size + mode;
        //设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
        // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
        }


        /* 通过详细测量结果获得mode */
        public static int getMode(int measureSpec) {
            // mode = measureSpec & MODE_MASK;
            // MODE_MASK = 11 00000000000(11后跟30个0)
            //原理:用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
            // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
            return (measureSpec & MODE_MASK);
        }



        /* 通过详细测量结果获得size */
        public static int getSize(int measureSpec) {
            // size = measureSpec & ~MODE_MASK;
            // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
            return (measureSpec & ~MODE_MASK);
        }

    }

    // 可以通过下面方式获取specMode和SpecSize
    //获取specModeint 
    specMode = MeasureSpec.getMode(measureSpec)

    //获取SpecSizeint 
    specSize = MeasureSpec.getSize(measureSpec)

    //也可以通过这两个值生成新的SpecModeint 
    measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
}

  子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。逻辑如下图:
这里写图片描述
接下来我们看下getChildMeasureSpec( )的源码分析:

    //作用:
    // 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
    //即子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性


    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

        /**参数说明
         * @param spec 父view的详细测量值(MeasureSpec)
         * @param padding view当前尺寸的的内边距和外边距(padding,margin)
         * @param childDimension 子视图的布局参数(宽/高)
         */

        //父view的测量模式
        int specMode = MeasureSpec.getMode(spec);

        //父view的大小
        int specSize = MeasureSpec.getSize(spec);

        //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
        int size = Math.max(0, specSize - padding);

        //子view想要的实际大小和模式(需要计算)
        int resultSize = 0;
        int resultMode = 0;

        //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小


        // 当父view的模式为EXACITY时,父view强加给子view确切的值
        //一般是父view设置为match_parent或者固定值的ViewGroup
        switch (specMode) {
            case MeasureSpec.EXACTLY:
        // 当子view的LayoutParams>0,即有确切的值
                if (childDimension >= 0) {
        //子view大小为子自身所赋的值,模式大小为EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;

        // 当子view的LayoutParams为MATCH_PARENT时(-1)
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
        //子view大小为父view大小,模式为EXACTLY
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;

        // 当子view的LayoutParams为WRAP_CONTENT时(-2)
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;

        // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)
            case MeasureSpec.AT_MOST:
        // 道理同上
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;

        // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
        // 多见于ListView、GridView
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
        // 子view大小为子自身所赋的值
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

  getChildMeasureSpec中对于子View测量模式和大小判断的逻辑较复杂,可以参考下图:
这里写图片描述
规律总结:(以子View为标准,横向观察)
(1) 当子View采用具体数值(dp / px)时
   无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;
(2) 当子View采用match_parent时
   子View的测量模式与父容器的测量模式一致
   若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间
(3) 当子View采用wrap_parent时
  无论父容器的测量模式是什么,子View的测量模式都是AT_MOST且大小不超过父容器的剩余空间。
(4) UNSPECIFIED模式:由于适用于系统内部多次measure情况,很少用到,故此处不讨论

3.2 measure过程

3.2.1 单个View的meaure过程

  在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。 这一过程如下图:
这里写图片描述
下面将对每个核心方法进行分析

measure()介绍
  这个方法的主要作用是测量逻辑的基本判断,然后调用onMeasure方法;属于final类型,所以子类不能重写此方法
源码如下:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

       //参数说明:View的宽 / 高测量规格
       ...
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
       // 计算视图大小
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
       ...

        }

onMeasure()介绍
这个方法用于调用getDefaultSize来定义View尺寸的测量逻辑和调用setMeasureDimension存储测量后的View的宽/高
源码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //参数说明:View的宽 / 高测量规格

        //setMeasuredDimension() 用于获得View宽/高的测量值
        //这两个参数是通过getDefaultSize()获得的
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

setMeasuredDimension()介绍
这个方法用于存储测量后的View的宽和高
源码如下:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {

        //参数说明:测量后子View的宽 / 高值

        //将测量后子View的宽 / 高值进行传递
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

getDefaultSize()介绍
这个方法会根据View宽和高的测量规格来计算View的宽高
源码如下:

public static int getDefaultSize(int size, int measureSpec) {

        //参数说明:// 第一个参数size:提供的默认大小// 第二个参数:宽/高的测量规格(含模式 & 测量大小)

        //设置默认大小
        int result = size;

        //获取宽/高测量规格的模式 & 测量大小
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        // 模式为UNSPECIFIED时,使用提供的默认大小
        // 即第一个参数:size
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
        // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值
        // 即measureSpec中的specSize
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }

        //返回View的宽/高值
        return result;
    }

  当模式是UNSPECIFIED时,使用的是提供的默认大小(即第一个参数size);默认大小是在onMeasure方法中,getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)中传入的默认大小是getSuggestedMinimumWidth()。

getSuggestedMinimumWidth方法的源码如下:

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
    }

    //getSuggestedMinimumHeight()同理

  从源码中可以看到,如果View没有设置背景,则View的宽度为mMinWidth,如果设置了背景,View的宽度为mMinWidth和mBackground.getMinimunWidth()中的最大值。
接下来看getMinimumWidth的源码:

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        //返回背景图Drawable的原始宽度
        return intrinsicWidth > 0 ? intrinsicWidth :0 ;
    }

  由源码可知,mBackground.getMinimumWidth()的大小具体是指背景图Drawable的原始宽度,如果没有原始宽度,则默认为0。
getDefaultSize计算View宽和高的逻辑如下图:
这里写图片描述

单个View的measure过程中对于每个方法的总结如下图:
这里写图片描述

3.2.2 ViewGroup的measure过程

  自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout(含有子View)。原理就是通过遍历所有的子View进行子View的测量,然后将所有的子View的尺寸进行合并,最终得到ViewGroup父视图的测量值。ViewGroup的measure过程如下图:
这里写图片描述
下面对每个方法进行分析:
measureChildren()方法
  和单一View的measure过程是从measure()开始不同,ViewGroup的measure过程是从measureChildren()开始的。ViewGroup是一个抽象类,自身并没有重写View的onMeasure方法。这个方法的主要作用就是遍历子View并调用measureChild进行下一步测量。
源码分析如下:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        //参数说明:父视图的测量规格(MeasureSpec)
        final int size = mChildrenCount;
        final View[] children = mChildren;

        //遍历所有的子view
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            //如果View的状态不是GONE就调用measureChild()去进行下一步的测量
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

measureChild()方法
  这个方法的主要作用是用于计算单个子View的MeasureSpec,调用子View的measure方法进行每个子View最后的宽高测量
源码分析如下:

protected void measureChild(View child, int parentWidthMeasureSpec,
                                int parentHeightMeasureSpec) {

        // 获取子视图的布局参数
        final LayoutParams lp = child.getLayoutParams();

        // 调用getChildMeasureSpec(),根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
        // getChildMeasureSpec()请回看上面的解析
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
                mPaddingTop + mPaddingBottom, lp.height);

        // 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}

measure()方法
这个方法和单个View的measure()方法是一致的,不再详细分析。

onMeasure()方法
  ViewGroup是一个抽象类,自身并没有重写View的onMeasure()方法。不同的ViewGroup子类(如LinearLayout,RelativeLayout等)具备不同的布局特性,这导致他们子View的测量方法各有不同,而onMeasure方法的作用在于测量View的宽高值,因此ViewGroup无法对onMeasure方法做统一实现。
  在定义ViewGroup中,关键在于根据自定义的View去复写onMeasure()从而实现你的子View测量逻辑。重写onMeasure的模板如下:

//根据自身的测量逻辑复写onMeasure()

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

     //定义存放测量后的View宽/高的变量
     int widthMeasure ;
     int heightMeasure ;


     //定义测量方法
     void measureCarson{
        //定义测量的具体逻辑
     }

     //记得!最后使用setMeasuredDimension() 存储测量后View宽/高的值
     setMeasuredDimension(widthMeasure, heightMeasure);
}

//最终setMeasuredDimension()会像上面单一View的measure过程中提到的,存储好测量后View宽/高的值并进行传递。

  单一View的measure过程与ViewGroup过程最大的不同:单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有。当然,在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需要进行更精细的测量。所以有时候也需要重写onMeasure()。

对每个方法的总结如下:
这里写图片描述

3.3 layout过程

layout阶段的基本思想也是由根View开始,递归地完成整个控件树的布局(layout)工作。
单个View的layout过程如下图:
这里写图片描述

ViewGroup的layout过程是先调用layout方法计算自身的位置,然后再遍历子View并调用子View layout()来确定自身子View的位置,过程如下:
这里写图片描述

我们把对decorView的layout()方法的调用作为布局整个控件树的起点,实际上调用的是View类的layout()方法,源码如下:

    public void layout(int l, int t, int r, int b) {
        // l为本View左边缘与父View左边缘的距离
        // t为本View上边缘与父View上边缘的距离
        // r为本View右边缘与父View左边缘的距离
        // b为本View下边缘与父View上边缘的距离
        . . .
        boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame
                (l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
        . . .

        }
        . . .
    }

  这个方法会调用setFrame()方法来设置View的mLeft、mTop、mRight和mBottom四个参数,这四个参数描述了View相对其父View的位置(分别赋值为l, t, r, b),在setFrame()方法中会判断View的位置是否发生了改变,若发生了改变,则需要对子View进行重新布局,对子View的局部是通过onLayout()方法实现了。由于普通View( 非ViewGroup)不含子View,所以View类的onLayout()方法为空。因此接下来,我们看看ViewGroup类的onLayout()方法的实现。
  实际上ViewGroup类的onLayout()方法是abstract,这是因为不同的布局管理器有着不同的布局方式。
  这里我们以decorView,也就是FrameLayout的onLayout()方法为例,分析ViewGroup的布局过程:

 @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childLeft;
                int childTop;
                int gravity = lp.gravity;

                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                                lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }

                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;

                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;

                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                                lp.topMargin - lp.bottomMargin;
                        break;

                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;

                    default:
                        childTop = parentTop + lp.topMargin;
                }
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

  在上面的方法中,parentLeft表示当前View为其子View显示区域指定的一个左边界,也就是子View显示区域的左边缘到父View的左边缘的距离,parentRight、parentTop、parentBottom的含义同理。确定了子View的显示区域后,接下来,用一个for循环来完成子View的布局。

  在确保子View的可见性不为GONE的情况下才会对其进行布局。首先会获取子View的LayoutParams、layoutDirection等一系列参数。上面代码中的childLeft代表了最终子View的左边缘距父View左边缘的距离,childTop代表了子View的上边缘距父View的上边缘的距离。会根据子View的layout_gravity的取值对childLeft和childTop做出不同的调整。最后会调用child.layout()方法对子View的位置参数进行设置,这时便转到了View.layout()方法的调用,若子View是容器View,则会递归地对其子View进行布局。

  到这里,layout阶段的大致流程我们就分析完了,这个阶段主要就是根据上一阶段得到的View的测量宽高来确定View的最终显示位置。显然,经过了measure阶段和layout阶段,我们已经确定好了View的大小和位置,那么接下来就可以开始绘制View了。

3.4 draw过程

绘制过程如下:
1. 绘制view背景
2. 绘制view内容
3. 绘制子View
4. 绘制装饰(渐变框,滑动条等等)

  对于本阶段的分析,我们以decorView.draw()作为分析的起点,也就是View.draw()方法,它的源码如下:

public void draw(Canvas canvas) {
        . . .
        // 绘制背景,只有dirtyOpaque为false时才进行绘制,下同
        int saveCount;
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        . . .

        // 绘制自身内容
        if (!dirtyOpaque) onDraw(canvas);

        // 绘制子View
        dispatchDraw(canvas);

        . . .
        // 绘制滚动条等
        onDrawForeground(canvas);

    }

  简单起见,在上面的代码中我们省略了实现滑动时渐变边框效果相关的逻辑。实际上,View类的onDraw()方法为空,因为每个View绘制自身的方式都不尽相同,对于decorView来说,由于它是容器View,所以它本身并没有什么要绘制的。dispatchDraw()方法用于绘制子View,显然普通View(非ViewGroup)并不能包含子View,所以View类中这个方法的实现为空。
  ViewGroup类的dispatchDraw()方法中会依次调用drawChild()方法来绘制子View,drawChild()方法的源码如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
      return child.draw(canvas, this, drawingTime);
}

  这个方法调用了View.draw(Canvas, ViewGroup,long)方法来对子View进行绘制。在draw(Canvas, ViewGroup, long)方法中,首先对canvas进行了一系列变换,以变换到将要被绘制的View的坐标系下。完成对canvas的变换后,便会调用View.draw(Canvas)方法进行实际的绘制工作,此时传入的canvas为经过变换的,在将被绘制View的坐标系下的canvas。进入View.draw(Canvas)方法后,会继续重复执行绘制过程的步骤。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值