android:id="@+id/tv_same"
android:layout_width="match_parent"
android:layout_height="50dp" />
在主布局中添加进去:
<?xml version="1.0" encoding="utf-8"?>
<!--include标签的使用-->
……
<include layout="@layout/layout_include2"/>
<include
android:id="@+id/view_same"
layout="@layout/layout_include2"/>
为了区分,这里给第二个layout_include2设置了id。也许你已经反应过来了,没错,我们就是要创建根布局的对象,然后再去初始化里面的控件:
TextView tvSame = findViewById(R.id.tv_same);
tvSame.setText("1.3 这里的TextView的ID是tv_same");
FrameLayout viewSame = findViewById(R.id.view_same);
TextView tvSame2 = viewSame.findViewById(R.id.tv_same);
tvSame2.setText("1.3 这里的TextView的ID也是tv_same");
运行之后可以看到这样的效果:
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F368%2F607%2F3686070456-5b9771ffaae61_articlex)
![image](//upload-images.jianshu.io/upload_images/15405197-725f843d608271e0?imageMogr2/auto-orient/strip%7CimageView2/2/w/432/format/webp)
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F368%2F607%2F3686070456-5b9771ffaae61_articlex)
可见虽然控件的id虽然相同,但是使用起来是没有冲突的。
# 2、merge
`include`标签虽然解决了布局重用的问题,却也带来了另外一个问题:布局嵌套。因为把需要重用的布局放到一个子布局之后就必须加一个根布局,如果你的主布局的根布局和你需要include的根布局都是一样的(比如都是`LinearLayout`),那么就相当于在中间多加了一层多余的布局了。那么有没有办法可以在使用`include`时不增加布局层级呢?答案当然是有的,那就是使用`merge`标签。
使用`merge`标签要注意一点:必须是一个布局文件中的根节点,看起来跟其他布局没什么区别,但它的特别之处在于页面加载时它的不会绘制的。打个比方,它就像是布局或者控件的搬运工,把“货物”搬到主布局之后就会功成身退,不会占用任何空间,因此也就不会增加布局层级了。这正如它的名字一样,只起“合并”作用。
## 2.1 merge常规使用
我们来验证一下,首先创建一个layout_merge.xml,在根节点使用`merge`标签:
<?xml version="1.0" encoding="utf-8"?>
<TextView
android:id="@+id/tv_merge1"
android:text="我是merge中的TextView1"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="40dp" />
<TextView
android:layout_toEndOf="@+id/tv_merge1"
android:id="@+id/tv_merge2"
android:text="我是merge中的TextView2"
android:background="@android:color/holo_blue_light"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="40dp" />
这里我使用了一些相对布局的属性,原因后面你就知道了。我们接着在ViewOptimizationActivity的布局添加RelativeLayout,然后使用include标签将layout_merge.xml添加进去:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/view_merge"
layout="@layout/layout_merge"/>
</RelativeLayout>
运行出来的效果图:
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F578%2F330%2F578330931-5b98dfef5f958_articlex)
![](//upload-images.jianshu.io/upload_images/15405197-0dc6eb0426020cc8?imageMogr2/auto-orient/strip%7CimageView2/2/w/431/format/webp)
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F578%2F330%2F578330931-5b98dfef5f958_articlex)
## 2.2 merge标签对布局层级的影响
在layout_merge.xml中,我们使用相对布局的属性`android:layout_toEndOf`将蓝色TextView设置到了绿色TextView的右边,而layout_merge.xml的父布局是`RelativeLayout`,所以这个属性是起了作用了,`merge`标签不会影响里面的控件,也不会增加布局层级。
如果你还不放心,可以用Android Studio来检查。我用的Android Studio是3.1版本的,可以通过**Layout Inspector**查看布局层级,不过记得要先在真机或者模拟器上把项目跑起来。依次点击Tools-Layout Inspector,然后选择你要查看的Activity,就可以看到如下的层级图:
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F712%2F816%2F712816485-5b986ca75a856_articlex)
![](//upload-images.jianshu.io/upload_images/15405197-a1a593db1cacd90c?imageMogr2/auto-orient/strip%7CimageView2/2/w/650/format/webp)
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F712%2F816%2F712816485-5b986ca75a856_articlex)
可以看到`RelativeLayout`下面直接就是两个TextView了, `merge`标签并没有增加布局层级。从这里也可以看出`merge`的局限性,即你需要明确将`merge`里面的布局和控件`include`到什么类型的布局中,才能提前设置好`merge`里面的布局和控件的位置。
## 2.3 merge的ID
在学习`include`标签时我们知道,它的`android:id`属性可以重写被include的根布局id,但如果根节点是`merge`呢?前面说了`merge`并不会作为一个布局绘制出来,所以这里给它设置id是不起作用的。我们可以在它的父布局`RelativeLayout`中再加一个TextView,使用`android:layout_below`属性把设置到layout_merge下面:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/view_merge"
layout="@layout/layout_merge"/>
<TextView
android:text="我不是merge中的布局"
android:layout_below="@+id/view_merge"
android:background="@android:color/holo_purple"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</RelativeLayout>
运行之后你会发现新加的TextView会把merge布局盖住,没有像预期那样在其下方。如果把`android:layout_below`中的id改为layout_merge.xml中任一TextView的id(比如tv_merge1),运行之后就可以看到如下效果:
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F165%2F246%2F1652465439-5b99d53f6b8a9_articlex)
![](//upload-images.jianshu.io/upload_images/15405197-61d004b0c087153b?imageMogr2/auto-orient/strip%7CimageView2/2/w/434/format/webp)
[](https://links.jianshu.com/go?to=https%3A%2F%2Fimage-static.segmentfault.com%2F165%2F246%2F1652465439-5b99d53f6b8a9_articlex)
这也符合2.2中的情况,即父布局`RelativeLayout`下级布局就是include进去的TextView了。
# 3、ViewStub
你一定遇到这样的情况:页面中有些布局在初始化时没必要显示,但是又不得不事先在布局文件中写好,虽然设置成了`invisible`或`gone`,但是在初始化时还是会加载,这无疑会影响页面加载速度。针对这一情况,Android为我们提供了一个利器————`ViewStub`。这是一个不可见的,大小为0的视图,具有懒加载的功能,它存在于视图层级中,但只会在`setVisibility()`和`inflate()`方法调用只会才会填充视图,所以不会影响初始化加载速度。它有以下三个重要属性:
* `android:layout`:ViewStub需要填充的视图名称,为“R.layout.xx”的形式;
* `android:inflateId`:重写被填充的视图的父布局id。
与`include`标签不同,`ViewStub`的`android:id`属性是设置`ViewStub`本身id的,而不是重写布局id,这一点可不要搞错了。另外,`ViewStub`还提供了`OnInflateListener`接口,用于监听布局是否已经加载了。
## 3.1 填充布局的正确方式
我们先创建一个layout_view_stub.xml,里面放置一个`Switch`开关:
<?xml version="1.0" encoding="utf-8"?>
然后在Activity的布局中修改如下:
<?xml version="1.0" encoding="utf-8"?>
<!--ViewStub标签的使用-->
<TextView
android:textSize="18sp"
android:text="3、ViewStub标签的使用"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ViewStub
android:id="@+id/view_stub"
android:inflatedId="@+id/view_inflate"
android:layout="@layout/layout_view_stub"
android:layout_width="match_parent"
android:layout_height="100dp" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:text="显示"
android:id="@+id/btn_show"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:text="隐藏"
android:id="@+id/btn_hide"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:text="操作父布局控件"
android:id="@+id/btn_control"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
在ViewOptimizationActivity中监听ViewStub的填充事件:
viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub viewStub, View view) {
Toast.makeText(ViewOptimizationActivity.this, "ViewStub加载了", Toast.LENGTH_SHORT).show();
}
});
然后通过按钮事件来填充和显示layout_view_stub:
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_show:
viewStub.inflate();
break;
case R.id.btn_hide:
viewStub.setVisibility(View.GONE);
break;
default:
break;
}
}
运行之后,点击“显示”按钮,layout_view_stub显示了,并弹出"ViewStub加载了"的Toast;点击“隐藏”按钮,布局又隐藏掉了,但是再点击一下“显示”按钮,页面居然却闪退了,查看日志,发现抛出了一个异常:
java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
我们打开ViewStub的源码,看看是哪里抛出这个异常的。很快我们就可以定位到是在inflate()方法中
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view = inflateViewNoAdd(parent);
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");
}
}
注意到if语句中有一个replaceSelfWithView()方法,听这名字就让人有一种不祥的预感了,点进去一看:
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
果然,ViewStub在这里调用了removeViewInLayout()方法把自己从布局移除了。到这里我们就明白了,ViewStub在填充布局成功之后就会自我销毁,再次调用inflate()方法就会抛出IllegalStateException异常了。此时如果想要再次显示布局,可以调用setVisibility()方法。
为了避免inflate()方法多次调用,我们可以采用如下三种方式:
**3.1.1 捕获异常**
我们可以捕获异常,同时调用setVisibility()方法显示布局。
try {
viewStub.inflate();
} catch (IllegalStateException e) {
Log.e("Tag",e.toString());
view.setVisibility(View.VISIBLE);
}
**3.1.2 通过监听ViewStub的填充事件**
声明一个布尔值变量isViewStubShow,默认值为false,布局填充成功之后,在监听事件onInflate方法中将其置为true。
最后是今天给大家分享的一些独家干货:
CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
【Android开发核心知识点笔记】
【Android思维脑图(技能树)】
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【Android高级架构视频学习资源】
CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
【Android开发核心知识点笔记】
[外链图片转存中…(img-3sRCdTlZ-1630914864639)]
【Android思维脑图(技能树)】
[外链图片转存中…(img-yx0KGgxP-1630914864641)]
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-Ba6OTrBa-1630914864643)]
【Android高级架构视频学习资源】
|