zoukankan      html  css  js  c++  java
  • Android UI 绘制过程浅析(一)LayoutInflater简介

    前言

      这篇blog是我在阅读过csdn大牛郭霖的《带你一步步深入了解View》一系列文章后,亲身实践并做出的小结。作为有志向的前端开发工程师,怎么可以不搞懂View绘制的基本原理——简直就像做后端却对数据库一无所知一样不可原谅!

      “纸上得来终觉浅,绝知此事要躬行。” 尽管自己对View的绘制仍然处于一知半解的程度,但凡事总要经过从0到1,方能从1到100。今天暂且记录下此时的理解与实践,作为千里之行中的小小一步。

    综述

      本篇blog先从自己平时最常用到的,在代码中引入布局文件的写法LayoutInflater.inflate讲起,探究inflate内部的机制;随后着手View绘制的三个阶段measure、layout、draw,看看一个View/ViewGroup是如何被绘制到屏幕上的;最后实现一个自定义的View。

    inflate

      初次接触inflate这个单词时,我在查阅词典后,知道了它的释义是“充气、膨胀”——老外定义函数名果然很恰当。想象一个没有充气的皮囊(布局文件),在充气(inflate)后变成了一件栩栩如生的女朋友立体物件(手机屏幕上的真实显示),相应地,LayoutInflater就被我翻译成了“打气筒”。布局文件我们每个人都很了解,无非是一个XXXLayout,内部再装上一些控件。那么问题来了,inflate方法内部是如何解析这一层层的View、决定它们在屏幕上显示的前后顺序、处理隐藏/显示逻辑的呢?下面是获得“打气筒inflater”的写法:

    1. 通过LayoutInflater提供的静态方法,从上下文中获取

    LayoutInflater inflater = LayoutInflater.from(context);

    2. 通过Service获取,上面的写法最终也是通过如下方法进行调用的

    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    inflate方法的三个参数

      在使用inflate方法时,我们都会注意到它需求三个参数

    public View inflate (int resource, ViewGroup root, boolean attachToRoot);

      另一种inflate方法不需要第三个attachToRoot参数,从源码上看,只是在上面方法的基础上进行了一次包装而已。

    public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
    }

       三个参数定义如下:

    • resource:布局文件id;not nullable
    • root:本次生成的布局外部嵌套的父布局;nullable
    • attachToRoot:是否将本次生成的布局加入到父布局

      不论调用何种包装后的方法,最终使用到的inflate方法如下:

        public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
    
                final AttributeSet attrs = Xml.asAttributeSet(parser);
                Context lastContext = (Context)mConstructorArgs[0];
                mConstructorArgs[0] = mContext;
                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("**************************");
                    }
    
                    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");
                        }
    
                        rInflate(parser, root, attrs, false, false);
                    } else {
                        // Temp is the root view that was found in the xml
                        final View temp = createViewFromTag(root, name, attrs, false);
    
                        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
                        rInflate(parser, temp, attrs, true, 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.
                        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.
                        if (root == null || !attachToRoot) {
                            result = temp;
                        }
                    }
    
                } catch (XmlPullParserException e) {
                    InflateException ex = new InflateException(e.getMessage());
                    ex.initCause(e);
                    throw ex;
                } catch (IOException e) {
                    InflateException ex = new InflateException(
                            parser.getPositionDescription()
                            + ": " + e.getMessage());
                    ex.initCause(e);
                    throw ex;
                } finally {
                    // Don't retain static reference on context.
                    mConstructorArgs[0] = lastContext;
                    mConstructorArgs[1] = null;
                }
    
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    
                return result;
            }
        }

       方法的注释中,有几处包含重要信息重要的地方:

    • 此方法依赖于编译阶段对xml文件的预处理,因此,在运行时修改XmlPullParser是行不通的
    • 参数root可选,当不为null时,起到的作用是为新生成的View提供LayoutParams的限制(在下一篇blog我们会看到,View绘制过程中,最终决定其尺寸的,是“父视图的支持尺寸”与“子视图的需求尺寸”)
    • 参数attachToRoot与参数root共同作用,只有 root非空&&attachToRoot==true 时,才会真正地发生attach;若 root==null && attachToRoot==true ,也不会发生attach行为

    inflate方法内部

      阅读上面的代码段,在inflate方法中,首先通过while循环找到开始Tag(这里使用了XmlPullParser,有兴趣的话可以深入去研究这个Xml解析接口,对于帮助理解XML文件结构很有帮助),如果发现这是一个merge节点,则调用rInflate(parser, root, attrs, false, false)。使用过merge节点的话,就会明白这是一个可以有效降低布局复杂度的技巧。这里暂时记下,后续我会专门写一篇blog研究merge(当前只需要记住一点,就是当使用merge节点时,该节点一定是根节点,且inflate的参数parent不为null,attachToRoot==true)。如果这个Tag不是merge节点,则首先根据这个Tag生成对应的View

    final View temp = createViewFromTag(root, name, attrs, false);

      createViewFromTag 所做的事情,主要是通过Tag的name,在当前Context的ClassLoader中加载对应的Class,拿到clazz后,通过newInstance生成目标View。可以看到不论首个Tag是否为merge,最终辗转都是走到rInflate方法中的,从方法命名开头的“r”就可以看出,这是一个递归解析xml的过程,如注释

    /**
        Recursive method used to descend down the xml hierarchy and instantiate views, instantiate their children, and then call onFinishInflate().
    */
        void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
                boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
                IOException {
    
            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");
                    }
                    parseInclude(parser, parent, attrs, inheritContext);
                } else if (TAG_MERGE.equals(name)) {
                    throw new InflateException("<merge /> must be the root element");
                } else {
                    final View view = createViewFromTag(parent, name, attrs, inheritContext);
                    final ViewGroup viewGroup = (ViewGroup) parent;
                    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                    rInflate(parser, view, attrs, true, true);
                    viewGroup.addView(view, params);
                }
            }
    
            if (finishInflate) parent.onFinishInflate();
        }

      方法后半部是递归调用的过程:首先使用之前提到的createViewFromTag方法生成一层View,接着以这个View作为Parent,去继续rInflate子View们,解析到最后,调用parent.onFinishInflate(),告知上层已经解析完成。

      至此为止,一个完整的inflate过程已经被解析完成了。下面我们结合具体的Demo代码,进一步巩固之前理解的知识。

    Demo

      很多新人在最初接触inflater时,往往困惑inflate方法后两个参数要怎么传,有时为了图方便,往往把 root = null, attachToRoot= false 一传了之。尽管大部分时间,这样处理是不会暴露出什么问题的,可是一旦习惯了这种写法,在真正出问题时就会一头雾水——“之前自己这么做明明没有问题的啊,这次怎么就不行了呢?!”。那么,我们就用下面这个demo来加深对inflate的理解。

      demo的布局很简单,一个空的FrameLayout,作为Activity的背景

      fake_main_activity.xml

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

       一个TextView的布局,准备将其安插在上面的FrameLayout里面

      textview_layout.xml

    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="@color/blue"
        android:text="Hello View!"
        android:textColor="@color/red"
        android:textSize="@dimen/text_size_34">
    
    </TextView>

      在FakeMainActivity中,如下设置页面布局,并加入上面的TextView

      FakeMainActivity.java

    package com.leili.imhere.activity;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    import com.leili.imhere.R;
    
    /**
     * 写blog专用测试Activity
     * Created by Lei.Li on 8/23/15 2:16 PM.
     */
    public class FakeMainActivity extends Activity {
        private ViewGroup fakeMainLayout;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            super.setContentView(R.layout.fake_main_activity);
            fakeMainLayout = (ViewGroup) super.findViewById(R.id.fake_main_layout);
    
    //        方法 1. root不为null,无需addView
            View textView = LayoutInflater.from(this).inflate(R.layout.textview_layout, fakeMainLayout);
    
    //        方法 2. root为null,手动addView
    //        View textView = LayoutInflater.from(this).inflate(R.layout.textview_layout, null);
    //        fakeMainLayout.addView(textView);
        }
    }

      可以看到这里我们使用了parent != null的inflate方法,屏幕截图是这样的,其中TextView的宽高均为300dp,符合我们的预期(在TextView布局文件里声明的宽高)

      如果我们注释掉方法1,使用方法2,即用 parent = null 来调用inflate,然后用ViewGroup.addView来将生成的View加入到外层FrameLayout中,会看到下面的屏幕截图

      是不是很奇怪?明明声明了TextView的宽高为300dp,这里它却占满了整个屏幕!造成这个现象的原因就在于inflate时没有为TextView声明ParentView,导致其layout_width/layout_height两个参数生效。在刚接触android时,我们往往会直观地把 layout_width/layout_height 这两个参数理解为View的宽与高,以为在布局文件里声明了多少的数值,最终绘制后就会出现多少的数值。这是有失偏颇的,要知道,这两个参数之所以不简单地叫做width/height,就是因为需要处理layout的过程。简言之layout_width/layout_height这两个参数,是用来告诉ParentView,自己需求多大的尺寸的。而如果在inflate时没有指明ParentView(如我们在方法2中所做的),子View就不知道该把layout_width/layout_height传给谁,这两个参数也就自然被忽略了。

      一个有趣的地方是,如果在上面的demo中,把最外层的FrameLayout改为LinearLayout,同样使用方法2(不指明ParentView),最终看到的是如下布局——TextView的宽度是match_parent,高度是wrap_content。究其原因,在于FrameLayout与LinearLayout这两种ViewGroup对于子View处理的方式不同。

      需要强调的一点是,当我们在Activity.onCreate()中使用 setContentView(int resId) 来设置页面背景时,Android系统为我们在外层自动嵌套了一个宽高都是满屏的FrameLayout,所以我们使用的背景资源文件根节点所声明的layout_width/layout_height是有效的。

    小结

      本篇blog从源码角度解析了 LayoutInflater 如何根据布局文件生成布局的过程——通过XmlParser递归地对xml文件进行解析,并佐以demo,指出了日常开发中一个容易产生潜在错误的用法。

      有了这里的基础,在下篇blog中,将迎来View绘制中最最核心的三个过程:measure、layout、draw,让我们一起拭目以待。

      

  • 相关阅读:
    7. JavaScript学习笔记——DOM
    6. Javscript学习笔记——BOM
    3. Javascript学习笔记——变量、内存、作用域
    2. Javscript学习笔记——引用类型
    1. JavaScript学习笔记——JS基础
    计算机网络学习笔记——网络层
    python小数据池,代码块知识
    pycharm快捷键
    字典
    04
  • 原文地址:https://www.cnblogs.com/maozhige/p/4751206.html
Copyright © 2011-2022 走看看