View绘制机制和LayoutInflater动态加载以及三种绘图界面更新区别

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sliverbullets/article/details/84039312

View绘制流程及机制

流程研究

场景:最外层自定义MaxViewGroup继承自LinearLayout+内层自定义ViewGroup继承自LinearLayout+自定义View
注:1.LinearLayout的onMearsure过程为两遍,每次调用View的onMeasure一遍。
2.RelativeLayout的onMeasure过程为三遍,每次调用View的onMeasure两遍

ViewGroup/View绘制过程:
START:
MaxViewGroup-onMeasure–>
ViewGroup–onMeasure–>
View–onMeasure–>
View–onMeasure完成–>
ViewGroup–onMeasure完成–>
MaxViewGroup–onMeasure完成–>
---------重复上面过程一遍---------
MaxViewGroup-onLayout–>
ViewGroup–onLayout–>
View–onLayout–>
View–onLayout完成–>
ViewGroup–onLayout完成–>
MaxViewGroup–onLayout完成–>
MaxViewGroup-draw
–>onDraw
–>onDraw完成–>
ViewGroup-draw
–>onDraw
–>onDraw完成–>
View-draw
–>onDraw
–>onDraw完成–>
draw完成–>
ViewGroup-draw完成–>
MaxViewGroup-draw完成–>
END

结论:①所以在不增加嵌套层数的时候LinearLayout比RelativeLayout的性能好,主要体现在测量过程上。
②绘制过程中测量顺序为父容器调用onMeasure然后依次调用子View的onMeasure,然后在最底层子View测量完成后在依次测量其父容器,(和View事件分发很像)。下来定位,父容器调用onLayout,然后依次调用子View的onLayout,然后在最底层子View定位完成后在依次定位其父容器的位置。最后绘制,父容器调用draw方法,draw会调用父容器的onDraw,在自己onDraw结束后会调用子View的draw方法,子view会调用自己的onDraw方法,这个方法完了后会调用子View的子View的draw方法,直到最底层的View调用draw方法,调用onDraw方法,onDraw完成,返回父容器的draw方法,父容器的draw方法完成,依次向上,最终完成整个View的绘制。
View的draw方法遵循如下规则:

1. Draw the background //绘制背景
2. If necessary, save the canvas’ layers to prepare for fading //如果有必要,保存画布的图层以备褪色
3. Draw view’s content //绘制视图的内容
4. Draw children //绘制子View
5. If necessary, draw the fading edges and restore layers //如有必要,绘制褪色边并还原图层
6. Draw decorations (scrollbars for instance) //绘制装饰(例如滚动条)

机制研究

1.onMeasure
measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。

MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型

  1. EXACTLY:表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

  2. AT_MOST:表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

  3. UNSPECIFIED:表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

view的measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小。最后在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。
ViewGroup有一个measureChildren()方法来去测量子视图的大小。

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。

2.onLayout
ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程,layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了layout()方法中,在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会调用onLayout()方法,但它是一个空方法,留给开发者自己实现。

onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了
getMeasureWidth()和getWidth()的区别:首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

3.onDraw
第一步对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。接下来的第三步 ,这一步的作用是对视图的内容进行绘制。调用了onDraw()方法,并且是一个空方法,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西。
主要参考:参考郭霖大神的博客(我只是简单整理了一下,因为他的博客讲的很详细了)

LayoutInflater工作原理

使用:

//inflate参数类型:int res,ViewGroup,boolean attahToRoot
Inflater inflater = LayoutInflater.from(getContext()).inflate(res,root,attahToRoot);

我们看一下inflate方法:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
	//一些初始化,传值等操作
        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;
            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();

            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                        + name);
                System.out.println("**************************");
            }
	//对marge类型xml进行检查是否符合要求,root不能为null并且attachToRoot要为true才可以
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                 //下面方法执行的条件:root不为空,attachToRoot为true,此时root就是根布局了
		//内部实现:递归方法加载root下的布局
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                //这个是根据tag创建View,即创建root
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied 创建与根匹配的布局参数(如果提供的话)
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                }

                // Inflate all children under temp against its context. 加载所有位于temp下的子View
                rInflateChildren(parser, temp, attrs, true);

                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                }

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                //当root不为null并且attachToRoot为true则把temp添加到root下
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                //没有root,并且attachToRoot(附加到root下)为false
                //则temp就是最终结果
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            final InflateException ie = new InflateException(e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (Exception e) {
            final InflateException ie = new InflateException(parser.getPositionDescription()
                    + ": " + e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        return result;
    }
}

在这个加载的方法里首先用pull的方法解析xml。然后根据root和attachToRoot这两参数分情况处理。
有以下情况:
1.root = null ,attachToRoot= true: 则temp为最终结果,在不是marge的情况下
2.root =null ,attachToRoot= false: 则temp为最终结果
3.root != null ,attachToRoot= true: 则root为最终结果
4.root != null ,attachToRoot= false: 则temp为最终结果,在不是marge的情况下
在不是mage标签的情况下,会调用createViewFromTag()这个方法,它把节点名和参数传了进去,它是根据节点名来创建View对象的然后返回值给temp。注意在createViewFromTag()方法的内部又会去调用createView()方法,然后使用反射的方式创建出View的实例并返回。当然,这里只是创建出了一个根布局的实例而已,接下来会调用rInflateChildren()方法其内部又调用rInflate()方法,来循环遍历这个根布局下的子元素并addView到ViewGroup中。最终完成了动态布局加载。

三种绘图界面更新

1.requesLayout()
子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。

2.invalidate()
该方法的调用会引起View树的重绘,常用于内部调用(比如 setVisiblity())或者需要刷新界面的时候,需要在主线程(即UI线程)中调用该方法,当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程由于没有添加measure和layout的标记位,因此measure、layout流程不会执行,而是直接从draw流程开始(只绘制需要重绘的视图)。

3.postInvalidate()
这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,这里用了Handler,发送了一个异步消息到主线程,这里发送的是MSG_INVALIDATE,即通知主线程刷新视图。

最后
一般来说,如果View确定自身不再适合当前区域,比如说它的LayoutParams发生了改变,需要父布局对其进行重新测量、布局、绘制这三个流程,往往使用requestLayout。而invalidate则是刷新当前View,使当前View进行重绘,不会进行测量、布局流程,因此如果View只需要重绘而不需要测量,布局的时候,使用invalidate方法往往比requestLayout方法更高效。

整理来源于此

猜你喜欢

转载自blog.csdn.net/sliverbullets/article/details/84039312