LayoutInflater是如何inflate创建view的?及merge、include、ViewStub标签使用和特点

setContentView最后通过LayoutInflater inflate出view

--> LayoutInflater.from(mContext).inflate(resId, contentParent);
	// 通过反射创建View  --- 布局的rootView
	--> final View temp = createViewFromTag(root, name, inflaterContext, attrs);
			// 是否是sdk
		--if (-1 == name.indexOf('.')) {  // LinearLayout 
                view = onCreateView(context, parent, name, attrs);
                	--PhoneLayoutInflater.onCreateView(name, attrs);
                		--> View view = createView(name, prefix, attrs);
            } else { // name = androidx.constraintlayout.widget.ConstraintLayout
                view = createView(context, name, null, attrs);
                		// 通过反射创建 View 对象
                	--> clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);
                	--> constructor = clazz.getConstructor(mConstructorSignature);
                	--> final View view = constructor.newInstance(args);
            }
    --> rInflateChildren(parser, temp, attrs, true); // 创建子View
    	--> rInflate
    		--> View view = createViewFromTag(parent, name, context, attrs);
		-->    final ViewGroup viewGroup = (ViewGroup) parent;
                	-->   final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                                -->   rInflateChildren(parser, view, attrs, true);
                                -->    viewGroup.addView(view, params)
                                
    // 通过构建全类命进行反射创建对象
	private static final String[] sClassPrefixList = {
								    "android.widget.",
								    "android.webkit.",
								    "android.app.",
								    "android.view."
								};     

LayoutInflate的参数的作用

下面四种方式可以通过阅读源码得到结果

--public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
                // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        // 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);
                        }
                    }
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);
                    // 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;
                    }
                }
public class InflateActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_inflate);
        LinearLayout ll = findViewById(R.id.ll);
        LayoutInflater inflater = LayoutInflater.from(this);
        // 方式一:将布局添加成功
//        View view = inflater.inflate(R.layout.inflate_layout, ll, true);

        // 方式二:报错,一个View只能有一个父亲(The specified child already has a parent.)
//        View view = inflater.inflate(R.layout.inflate_layout, ll, true); // 已经addView
//        ll.addView(view);

        // 方式三:布局成功,第三个参数为false
        // 目的:想要 inflate_layout 的根节点的属性(宽高)有效,又不想让其处于某一个容器中
//        View view = inflater.inflate(R.layout.inflate_layout, ll, false);
//        ll.addView(view);

        // 方式四:root = null,这个时候不管第三个参数是什么,显示效果一样
        // inflate_layout 的根节点的属性(宽高)设置无效,只是包裹子View,
        // 但是子View(Button)有效,因为Button是出于容器下的
        // View 没有父容器  ---》 尺寸大小 参数的设置无效  View的绘制流程
        View view = inflater.inflate(R.layout.inflate_layout, null, false);
        ll.addView(view);  
    }
}

merge、include、ViewStub标签的特点

include标签

标签在布局优化中是使用最多的一个标签了,它就是为了解决重复定义布局的问题。标签就相当于C、C++中的include头文件一样,把一些常用的底层的API封装起来,需要的时候引入即可。在一些开源的J2EE中许多XML配置文件也都会使用标签,将多个配置文件组合成为一个更为复杂的配置文件,如最常见的S2SH。在以前Android开发中,由于ActionBar设计上的不统一以及兼容性问题,所以很多应用都自定义了一套自己的标题栏titlebar。标题栏我们知道在应用的每个界面几乎都会用到,在这里可以作为一个很好的示例来解释标签的使用。下面是一个自定义的titlebar文件:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:background="@color/titlebar_bg">
	<ImageView android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:src="@drawable/gafricalogo" />
</FrameLayout>

在应用中使用titlebar布局文件,我们通过标签,布局文件如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:background="@color/app_bg"
	android:gravity="center_horizontal">
	<include layout="@layout/titlebar"/>
	<TextView android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:text="@string/hello"
android:padding="10dp" />
</LinearLayout>

在标签中可以覆盖导入的布局文件root布局的布局属性(如layout_*属性)。
布局示例如下:

<include android:id="@+id/news_title"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	layout="@layout/title"/>

如果想使用标签覆盖嵌入布局root布局属性,必须同时覆盖layout_height和layout_width属性,否则会直接报编译时语法错误。如果标签已经定义了id,而嵌入布局文件的root布局文件也定义了id,标签的id会覆盖掉嵌入布局文件root的id,如果include标签没有定义id则会使用嵌入文件root的id。

include:
1. 不能作为根元素,需要放在 ViewGroup2. findViewById查找不到目标控件,这个问题出现的前提是在使用include时设置了id,而在findViewById时却用了被include进来的布局的根元素id。
   1. 为什么会报空指针呢?
   如果使用include标签时设置了id,这个id就会覆盖 layout根view中设置的id,从而找不到这个id
   --rInflate(XmlPullParser parser, View parent, Context context,AttributeSet attrs, boolean finishInflate)
	--else if (TAG_INCLUDE.equals(name)
		--parseInclude(parser, context, parent, attrs);
			--final View view = createViewFromTag(parent, childName,context, childAttrs, hasThemeOverride); 实际include的布局
			            --》     view.setLayoutParams(params);
			            --if (id != View.NO_ID) {
                     			   	 view.setId(id);
                  			 	}
			           --》      group.addView(view);	把实际include的布局加入root	

merge标签

标签都是与标签组合使用的,它的作用就是可以有效减少View树的层次来优化布局。下面通过一个简单的示例探讨一下标签的使用:
一个线性布局中嵌套一个文本视图,主布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/layout_wrap"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:orientation="vertical" >
	<include
	android:id="@+id/layout_import"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	layout="@layout/layout_text" />
</LinearLayout>

下面是嵌套布局的layout_text.xml文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:orientation="vertical" >
	<TextView
	android:id="@+id/textView"
	android:layout_width="match_parent"
	android:text="Hello World!"
	android:layout_height="match_parent" />
</LinearLayout>

通过hierarchyviewer我们可以看到主布局View树的部分层级结构如下图:
在这里插入图片描述现在讲嵌套布局跟布局标签更改为,merge_text.xml布局文件如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android">
	<TextView
	android:id="@+id/textView"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:text="Hello World!"/>
</merge>

当然merge可以

<?xml version="1.0" encoding="utf-8"?>
<merge>
    <TextView/>
    <TextView/>
    <TextView/>
</merge>

然后将主布局标签中的layout更改为merge_text.xml,运行后重新截图如下:
在这里插入图片描述
对比截图就可以发现上面的四层结构,现在已经是三层结构了。当我们使用标签的时候,系统会自动忽略merge层级,而把TextView直接放置与平级。标签在使用的时候需要特别注意布局的类型,例如我的标签中包含的是一个LinearLayout布局视图,布局中的元素是线性排列的,如果嵌套进主布局时,include标签父布局时FrameLayout,这种方式嵌套肯定会出问题的,merge中元素会按照FrameLayout布局方式显示。所以在使用的时候,标签虽然可以减少布局层级,但是它的限制也不可小觑。只能作为XML布局的根标签使用。当Inflate以开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true。View android.view.LayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot) root不可少,attachToRoot必须为true。

merge:
1. merge标签必须使用在根布局作为rootView
--rInflate(XmlPullParser parser, View parent, Context context,AttributeSet attrs, boolean finishInflate)
	--else if (TAG_MERGE.equals(name)) {
                	throw new InflateException("<merge /> must be the root element");
         	               } 
2. 因为merge标签并不是View,所以在通过LayoutInflate.inflate()方法渲染的时候,第二个参数必须指定一个父容器,
	且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点.
3. 由于merge不是View所以对merge标签设置的所有属性都是无效的.
4. 优化布局,减少层级

ViewStub

在开发过程中,经常会遇到这样一种情况,有些布局很复杂但是却很少使用。例如
条目详情、进度条标识或者未读消息等,这些情况如果在一开始初始化,虽然设置
可见性 View.GONE ,但是在Inflate的时候View仍然会被Inflate,仍然会创建对象,
由于这些布局又想到复杂,所以会很消耗系统资源。
ViewStub就是为了解决上面问题的,ViewStub是一个轻量级的View,它一个看不
见的,不占布局位置,占用资源非常小的控件。

定义ViewStub布局文件

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

    <ViewStub
        android:id="@+id/stub_import"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:inflatedId="@+id/inflate_stub"
        android:layout="@layout/view_stub_layout" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我显示在ViewStub下面" />

</LinearLayout>

layout/view_stub_layout布局文件

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

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是ViewStub的layout" />

</LinearLayout>

加载ViewStub布局文件

// ViewStub 标签讲解
public class ViewStubActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // main.xml中包含上面的ViewStub
        setContentView(R.layout.activity_view_stub);

        // 获取ViewStub,
        ViewStub viewStub = (ViewStub) findViewById(R.id.stub_import);

        // 显示 ViewStub:使用setVisibility和inflate 都可以
        viewStub.setVisibility(View.VISIBLE);
//        viewStub.inflate();

        // 注意这里是通过ViewStub的inflatedId来获取
        // 因为设置了 inflatedId,所以通过layout根view的id获取会报空指针异常
//        LinearLayout linearLayout = findViewById(R.id.ll_tv);

//        TextView tv = linearLayout.findViewById(R.id.tv);
//        tv.setText("设置成功");

//        if (viewStub.getVisibility() == View.VISIBLE) {
//            // 已经加载, 否则还没有加载
//            Log.e("leo", "onCreate: 是否显示" );
//        }
    }
}

ViewStub一旦visible/inflated,它自己就不在是View试图层级的一部分了。所以后面无法再使用ViewStub来控制布局,填充布局root布局如果有id,则会默认被android:inflatedId所设置的id取代,如果没有设置android:inflatedId,则会直接使用填充布局id。由于ViewStub这种使用后即可就置空的策略,所以当需要在运行时不止一次的显示和隐藏某个布局,那么ViewStub是做不到的。这时就只能使用View的可见性来控制了。layout_*相关属性与include标签相似,如果使用应该在ViewStub上面使用,否则使用在嵌套进来布局root上面无效。ViewStub的另一个缺点就是目前还不支持merge标签。

ViewStub:就是一个宽高都为0的一个View,它默认是不可见的
1. 类似include,但是一个不可见的View类,用于在运行时按需懒加载资源,只有在代码中调用了viewStub.inflate()
	或者viewStub.setVisible(View.visible)方法时才内容才变得可见。
2. 这里需要注意的一点是,当ViewStub被inflate到parent时,ViewStub就被remove掉了,即当前view hierarchy中不再存在ViewStub,
	而是使用对应的layout视图代替。

ViewStub.java
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
	// 获取 InflatedId
	mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
	// 设置不显示
    setVisibility(GONE);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// 设置宽高为0
    setMeasuredDimension(0, 0);
}
public View inflate() {
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            // 设置ID
            final View view = inflateViewNoAdd(parent);
            // 替换自己和View
            replaceSelfWithView(view, parent);

            mInflatedViewRef = new WeakReference<>(view);
            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }

            return view;
        } else {
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}

private View inflateViewNoAdd(ViewGroup parent) {
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
}

private void replaceSelfWithView(View view, ViewGroup parent) {
    final int index = parent.indexOfChild(this);
    // 移除自己
    parent.removeViewInLayout(this);

    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    // 添加View
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
        parent.addView(view, index);
    }
}
  • 31
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值