写在前面

本次分析的源码的SDK的版本为25.3.1.

ViewStub的简单使用

可以查看官方文档的相关说明.

在xml中使用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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.houtrry.viewstubdemo.MainActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button"
android:textAllCaps="false"
android:textSize="18sp"/>

<ViewStub
android:id="@+id/viewStub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inflatedId="@+id/ll_root"
android:layout="@layout/layout_stub"/>

</LinearLayout>

其中, android:layout=”@layout/layout_stub”是关联布局layout_stub, 即是使用@layout/layout_stub来填充布局.android:inflatedId=”@+id/ll_root”的作用稍后说明.

layout/layout_stub如下

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"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher_round"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hello world!"
android:textAllCaps="false"
android:textColor="@color/colorAccent"
android:textSize="15sp"/>

</LinearLayout>

在Activity中控制ViewStub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private ViewStub mViewStub;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mViewStub = (ViewStub) findViewById(R.id.viewStub);

LinearLayout ll_root = (LinearLayout) findViewById(R.id.ll_root);
Log.d(TAG, "onCreate: ll_root: "+ll_root);

findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mViewStub.inflate();

LinearLayout ll_root = (LinearLayout) findViewById(R.id.ll_root);
Log.d(TAG, "onCreate: ll_root: "+ll_root);
}
});
}

ViewStub#inflate调用前, ViewStub没有任何显示.ViewStub#inflate调用后, 显示出layout/layout_stub的内容.
关于android:inflatedId=”@+id/ll_root”, ll_root就是layout_stub的id, 可以使用inflatedId的值来获取android:layout=”@layout/layout_stub”布局的根控件.
点击按钮后, 日志如下:

1
2
D/MainActivity: onCreate: ll_root: null
D/MainActivity: onCreate: ll_root: android.widget.LinearLayout{180892 V.E..... ......I. 0,0-0,0 #7f0b0060 app:id/ll_root}

说明调用ViewStub#inflate前, 布局layout/layout_stub并没有填充, 也没法获取layout/layout_stub中的控件.ViewStub#inflate后才可以.
再次点击按钮, 程序崩溃, 崩溃信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.houtrry.viewstubdemo, PID: 4841
java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
at android.view.ViewStub.inflate(ViewStub.java:292)
at com.houtrry.viewstubdemo.MainActivity$1.onClick(MainActivity.java:28)
at android.view.View.performClick(View.java:4785)
at android.view.View$PerformClick.run(View.java:19884)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5343)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:905)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:700)

提示ViewStub没有父控件. 说明ViewStub#inflate后, 不能再次调用ViewStub#inflate方法. 至于原因, 后面说明.


源码分析

源码内容

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
@RemoteView
public final class ViewStub extends View {
private int mInflatedId;
private int mLayoutResource;

private WeakReference<View> mInflatedViewRef;

private LayoutInflater mInflater;
private OnInflateListener mInflateListener;

public ViewStub(Context context) {
this(context, 0);
}

public ViewStub(Context context, @LayoutRes int layoutResource) {
this(context, null);

mLayoutResource = layoutResource;
}

public ViewStub(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);

final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
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);
}

@IdRes
public int getInflatedId() {
return mInflatedId;
}

@android.view.RemotableViewMethod
public void setInflatedId(@IdRes int inflatedId) {
mInflatedId = inflatedId;
}

@LayoutRes
public int getLayoutResource() {
return mLayoutResource;
}

@android.view.RemotableViewMethod
public void setLayoutResource(@LayoutRes int layoutResource) {
mLayoutResource = layoutResource;
}

public void setLayoutInflater(LayoutInflater inflater) {
mInflater = inflater;
}

public LayoutInflater getLayoutInflater() {
return mInflater;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}

@Override
public void draw(Canvas canvas) {
}

@Override
protected void dispatchDraw(Canvas canvas) {
}

@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) {
inflate();
}
}
}

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);
}
final View view = factory.inflate(mLayoutResource, parent,
false);

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");
}
}

public void setOnInflateListener(OnInflateListener inflateListener) {
mInflateListener = inflateListener;
}

public static interface OnInflateListener {
void onInflate(ViewStub stub, View inflated);
}
}

构造方法

构造方法中, 我们获取了属性android:inflatedId 和android:layout的值, 并将ViewStub设置为隐藏.

ViewStub#inflate

首先, 获取ViewStub的parent, 如果parent为空或者parent不是ViewGroup, 则抛出异常”ViewStub must have a non-null ViewGroup viewParent”, 我们先把问题放在这里, 继续向下看.
mLayoutResource就是android:layout的值, 如果没有设置android:layout, 则抛出异常”ViewStub must have a valid layoutResource”.
接下来就是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
//找到ViewStub的父控件
final ViewGroup parent = (ViewGroup) viewParent;
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
//通过LayoutInflater获取android:layout的View
final View view = factory.inflate(mLayoutResource, parent,
false);
//如果有设置android:inflatedId, 将android:inflatedId的值设置给填充的View, 这也是为什么我们能通过
//LinearLayout ll_root = (LinearLayout) findViewById(R.id.ll_root)获取layout/layout_stub的根控件
//LinearLayout的原因
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
//获取ViewStub在父控件中的位置
final int index = parent.indexOfChild(this);
//将ViewStub从父控件中移除!!!.
//这里就是ViewStub#inflate调用第二次的时候出现"ViewStub must have a non-null ViewGroup viewParent"
//异常的原因. 因为ViewStub#inflate执行后, ViewStub就被移除了, parent就是null.
parent.removeViewInLayout(this);
//在父控件的相同位置添加填充的View
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}

再往后, 就是用弱引用保存填充的View了, 以及执行InflateListener的监听回调.

ViewStub#setVisibility

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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) {
inflate();
}
}
}

这就很简单了, 从弱引用中取出View, 调用View的setVisibility方法.

ViewStub#setLayoutResource和ViewStub#setInflatedId

1
2
3
4
5
6
7
8
9
@android.view.RemotableViewMethod
public void setInflatedId(@IdRes int inflatedId) {
mInflatedId = inflatedId;
}

@android.view.RemotableViewMethod
public void setLayoutResource(@LayoutRes int layoutResource) {
mLayoutResource = layoutResource;
}

这个很简单了, 在调用ViewStub#inflate前, 我们可以在代码中设置android:inflatedId 和android:layout的值.
应该注意的是, ViewStub#inflate只能调用一次, 在ViewStub#inflate后调用ViewStub#setLayoutResource和ViewStub#setInflatedId将不会有任何效果.

总结

  1. ViewStub的原理是初始化的时候隐藏. ViewStub#inflate后, 将自己从父控件中移除, 并将填充View添加到ViewStub所在的位置进行显示.
  2. ViewStub#inflate只能调用一次. 调用一次后再调用的话, 会抛出异常”ViewStub must have a non-null ViewGroup viewParent”.
  3. ViewStub#inflate后, 使用ViewStub#setVisibility控制控件的显示与隐藏(实际调用的是填充View的显示与隐藏)