ViewStub其实本质上也是一个View,其继承关系如图所示:
ViewStub使用的是惰性加载的方式,即使将其放置于布局文件中,如果没有进行加载那就为空,不像其它控件一样只要布局文件中声明就会存在。
那ViewStub适用于场景呢?通常用于网络请求页面失败的显示。一般情况下若要实现一个网络请求失败的页面,我们是不是使用两个View呢,一个隐藏,一个显示。试想一下,如果网络状况良好,并不需要加载失败页面,但是此页面确确实实已经加载完了,无非只是隐藏看不见而已。如果使用ViewStub,在需要的时候才进行加载,不就达到节约内存提高性能的目的了吗?
ViewStub只能加载一次,重复加载会导致异常,这是因为ViewStub只要加载过一次,其自身就会被移除,把并自身所包含的内容全部传给父布局。来张图感受一下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context="com.example.administrator.myviewstub.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="inflate"
android:text="inflate" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="setData"
android:text="setdata"/>
<ViewStub
android:id="@+id/vs"
android:layout="@layout/viewstub"
android:layout_width="match_parent"
android:layout_height="match_parent" />
LinearLayout>
可以看到ViewStub又引用了另外一个布局。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/hello_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DATA EMPTY!"/>
FrameLayout>
public class MainActivity extends AppCompatActivity {
private ViewStub viewStub;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewStub = (ViewStub) findViewById(R.id.vs);
//textView = (TextView) findViewById(R.id.hello_tv);空指针
}
public void inflate(View view){
viewStub.inflate();
//textView = (TextView) viewStub.findViewById(R.id.hello_tv);空指针
textView = (TextView) findViewById(R.id.hello_tv);
}
public void setData(View view){
textView.setText("DATA!!");
}
}
注意:这里有几个坑,前面我们说了ViewStub只有在初始化之后才会存在,所以第一个注释中的空指针是因为ViewStub还未初始化。那第二个注释中的空指针是怎么回事呢?前面我们也说了,ViewStub在加载完成之后会移除自身,并把自身的内容转给父布局,所以此时viewStub中的内容已经不存在了,textView已经是父布局的东西了,所以不能使用viewStub来findViewById。另外,前面我们说了ViewStub只能加载一次,若调用两次inflate()的话会导致异常。
为了验证所得出的结论是不是正确的,我截了两张ViewStub加载前后的图。
从图中可以看到ViewStub还没加载,是灰色的。
当点击了INFLATE之后,可以看到,ViewStub消失了,取而代之的是一个FrameLayout,其中包含了一个AppCompatTextView(ps.其实就是TextView,只是Google在5.0之后提供了向前兼容,就好比AppCompatActivity和Activity一样)。咳,这个FrameLayout是不是很眼熟,没错!就是我们之前写的ViewStub的布局,忘记的童鞋翻回去看看。
没有问题。
关于这个图是怎么来的,童鞋们只要点击Android Montior中的Layout Inspector就行啦,就是介个:
感兴趣的童鞋可以自己去试试。
ViewStub是如何实现的呢,接下来我们来一探究竟:
public View inflate() {
final ViewParent viewParent = getParent();//获取父View
if (viewParent != null && viewParent instanceof ViewGroup) {
//若父不是ViewGroup就会抛出异常("ViewStub must have a non-null ViewGroup viewParent")
if (mLayoutResource != 0) {
//这个就是ViewStub只能加载一次的原因,第二次加载则抛出异常(throw new IllegalArgumentException("ViewStub must have a valid layoutResource"))
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);
//创建一个View,这个View为ViewStub的内容,mLayoutResource为ViewStub自身的Layout资源文件id
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
//若mInflatedId存在,则将id重新赋值给新的View
}
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);
//通过父布局将ViewStub移除
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
//将ViewStub中的内容添加到父容器中
mInflatedViewRef = new WeakReference(view);
//设置为弱引用,当VIewStub设置为空时将立即执行GC
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
return view;
//最后将View返回出去
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
ViewStub中还有一个setVisibility(int visibility)值得我们注意:
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
View view = mInflatedViewRef.get();
//拿到关联的Layout
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();
}
}
}
可以看到,ViewStub若之前没有进行inflate,setVisibility(View.VISIBLE)或setVisibility(View.INVISIBLE)会自动先进行加载,而加载之后可以设置显示和隐藏。并且ViewStub设置的不是自己,而是拿到关联的那个Layout设置visible。ViewStub此时并未销毁,所以建议初始化后将其设置为空。