使用include、merge、ViewStub来优化你的布局

在开发中UI布局是我们都会遇到的问题,随着UI越来越多,布局的重复性、复杂度也会随之增长,我们怎样布局才是最合理,性能最好的呢?Android官方也给出了很多优化布局的方案,下面我主要讲解includemergeViewStub在优化布局中的使用方法

include

include想必大家最熟悉了,它的用处就是当我们有一个布局在很多地方都用到的时候,比如divider,toolbar,我们可以将这个布局抽离出一个单独的xml文件,然后在其他需要用到它的时候include进来即可,简单来说就是布局重用,下面贴一个简单的例子toolbar.xml

1
2
3
4
5
6
7
8
9
10
11
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/window_background"
android:orientation="vertical">
<include layout="@layout/toolbar"/>
...
</LinearLayout>

这里的toolbar就是被抽离出来的布局,这样我们在其他Activity需要加入toolbar的时候直接includetoolbar.xml就可以了

merge

merge标签的作用在于通过减少view的层级从而消除冗余ViewGroup,达到布局优化的目的.下面举一个购物车列表的例子

  1. 当购物车为空时,显示view_order_none.xml
  2. 当购物车不为空时,显示列表ListView

下面贴一下布局文件的源码fragment_cart.xml,方便大家理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/vs_cart_none"
android:inflatedId="@+id/stub_fl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/view_order_none"/>
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
其中view_order_none.xml代码如下
<FrameLayout
android:id="@+id/fl_cart_none"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

这里fragment_cart.xml包含了两个布局,ViewStub(下面会介绍)和ListView,他们通过一个FramLayout包裹起来,其实我们再仔细一想,这个FrameLayout的存在有没有必要呢?结果是没有必要的,它只会让我们层级又多了一层,这个时候就可以用merge标签替换它了,这也是merge的使用场景,用merge替换后的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/vs_cart_none"
android:inflatedId="@+id/stub_fl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/view_order_none"/>
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge>

ViewStub

ViewStub标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。各种不常用的布局像进度条、显示错误消息等可以使用ViewStub标签,以减少内存使用量,加快渲染速度。继续上面购物车列表为空的例子

判断ViewStub是否加载

1
2
3
4
5
6
7
8
9
10
11
ViewStub vs_cart_none = (ViewStub) findViewById(R.id.vs_cart_none);
// 加载购物车列表布局
vs_cart_none.setVisibility(View.VISIBLE);
// 获取到购物车ListView,注意这里是通过ViewStub的inflatedId来获取
FrameLayout stub_fl = findViewById(R.id.stub_fl);
if (vs_cart_none.getVisibility() == View.VISIBLE ) {
// 已经加载, 否则还没有加载
stub_fl.setVisibility(View.GONE);
}else{
stub_fl.setVisibility(View.VISIBLE);
}

上面我们是通过vs_cart_none.setVisibility(View.VISIBLE); 来加载ViewStub的布局的,还有另外一种方法

FrameLayout stub_fl;
...
ViewStub vs_cart_none = (ViewStub) findViewById(R.id.vs_cart_none);
if(stub_fl==null){
       //加载购物车布局
       stub_fl = (ListView)vs_cart_none.inflate();
}else{
}

注意这两种加载方法的判断逻辑是不一样的

还有一点 我们的ViewStub布局中有这个么一个属性,android:inflatedId=”@+id/stub_fl”,在判断ViewStub是否加载的第一种方法中,我们是通过这个inflatedId来找到FrameLayout的,开始我也举得很疑惑,我们的FrameLayout不是有自己的idstub_fl吗?为什么要通过这个inflatedId来找,如果这两个id同时存在,该通过那个id来找,带着疑问我们去看下ViewStub的源码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
//获取inflatedId
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
setVisibility(GONE);//设置不可见
setWillNotDraw(true);//设置不绘制
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);//宽高默认为0
}
/**
* When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
* {@link #inflate()} is invoked and this StubbedView is replaced in its parent
* by the inflated layout resource. After that calls to this function are passed
* through to the inflated view.
*
* @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
*
* @see #inflate()
*/
@Override
@android.view.RemotableViewMethod
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
//已经加载只设置是否可见
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
//VISIBLE或INVISIBLE条件下就加载
inflate();
}
}
}
/**
* Inflates the layout resource identified by {@link #getLayoutResource()}
* and replaces this StubbedView in its parent by the inflated layout resource.
*
* @return The inflated layout resource.
*
*/
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
//此处的view为layout包裹的布局的根布局
final View view = factory.inflate(mLayoutResource, parent,
false);
//如果布局中设置了inflatedId,就将这个id赋给view
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
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);
}
mInflatedViewRef = new WeakReference<View>(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");
}
}

所以如果ViewStub属性中设置了android:inflatedId:stub_fl,那么view_order_none.xml中的android:id="@+id/fl_cart_none"就无效了,否则就可以通过fl_cart_none取到

总结

通过上面的介绍,我想大家应该大概了解include、merge、VIewStub的使用方法了,其实并不是有多难,只是我们很少用他们而已,我们在做性能优化优化的时候,UI布局的优化也是其中的一部分,那么让我们用上它们吧!

参考

http://developer.android.com/intl/zh-cn/training/improving-layouts/reusing-layouts.html
http://www.devtf.cn/?p=509