本篇博客主要解决以下问题:
1、ViewStub是什么?怎么用?
2、为什么ViewStub是懒加载的?它和把View设置为GONE有什么区别?
一、ViewStub使用
先说一下ViewStub的应用场景及使用:
假如页面上有一个数据显示的View(contentView),还有一个针对异常处理的View(errorView),正常情况下,我们在打开这个界面的时候只有contentView是可见的,errorView隐藏,当出现异常情况时,会把errorView设置为可见,而contentView隐藏。这里就有一个问题了,errorView和contentView都是在一个布局文件里,android的View加载机制告诉我们,LayoutInflater会同时加载这两个View,造成了资源的浪费,能不能有一种办法,使得errorView在一开始的时候不加载,只在需要的时候再加载呢,于是ViewStub横空出世,它可以实现初始时不加载,需要用到某个view或views时才被加载并使用!
以上面的场景为例,用法如下:
1、在Activity的布局文件里声明一个ViewStub,其中的layout属性指向errorView的布局文件:
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_viewstub" />
2、在Activity中通过StubView的inflate()方法加载我们的errorView,同时对其中的控件进行初始化:
ViewStub viewStub = findViewById(R.id.view_stub);
//此时的view相当于被加载布局的根部局
View view = viewStub.inflate();
TextView tvInfo = view.findViewById(R.id.tv_error);
tvInfo.setOnClickListener(this);
3、加载完成后,就可以跟正常的View一样进行处理了~
注意:
1、ViewStub的 inflate()只能使用一次。
2、ViewStub所要替代的layout文件中不能有<merge>
标签。
二、源码解析
ViewStub本身也是继承自View,首先看构造函数:
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);
}
可以看到,构造ViewStub的时候设置了Visibility为GONE,同时读取layout属性,将布局的线上服务存到 mLayoutResource中。然后看onMeasure()和onDraw()方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
测量的时候将自身设置为大小为0的view,且不绘制。
它只是一个大小为0,不显示、不绘制的View。
接着看它的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");
}
}
查看inflateViewNoAdd()方法:
private View inflateViewNoAdd(ViewGroup parent) {
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);
}
return view;
}
原来,ViewStub是通过 LayoutInflater将指定的布局加载进来,那它是怎么在界面上显示呢:
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);
}
}
replaceSelfWithView()方法给出了答案:
它是获取到parentView,将自身从parentView中移除,然后通过addView()将指定的layout动态添加进去的。相当于一开始它只占了个坑,没有内容。有人要用到这个坑时,就把自己的坑让出来~同时也可以明白,它只能inflate()一次,是因为inflate()之后会把自己移除,在获取不到parentView的时候会抛出异常。
三、ViewStub和INVISIBLE、GONE的区别
1、invisible
view设置为invisible时,view在layout布局文件中会占用位置,但是view为不可见,该view还是会创建对象,会被初始化,会占用资源。
2、gone
view设置gone时,view在layout布局文件中不占用位置,但是该view还是会创建对象,会被初始化,会占用资源。
3、viewstub
viewstub是一个轻量级的view,它不可见,不用占用资源,只有设置viewstub为visible或者调用其inflater()方法时,其对应的布局文件才会被初始化。但是viewstub的引用对象需要是一个布局layout文件,如果要是单个的view的话,viewstub就不能满足要求,就需要调用view的gone或者invisible属性了。