概述
-
有时需要某个布局在一开始不显示,在某个条件下才显示,可以通过visable属性来控制,但这样效率非常低,因为虽然布局隐藏来,但还在布局中,仍会解析这些布局。可以使用ViewStub控件来解决这个场景并提高效率。
-
“懒加载”就是为了让程序尽可能快地启动而提出的一个优化策略,即让那些对用户不重要或者不需要立即显示的布局控件做延迟加载,只在需要显示的时候才进行加载,这样就可以让程序在启动显示的过程中加载更少的控件,占用更少的内存空间,从而更快启动并显示。
Android系统也提供了一种用于实现布局控件懒加载的工具ViewStub,它能够让相关布局在显示时再进行加载,从而提升程序启动速度。 -
==ViewStub是一个轻量级的View,它是一个看不见的,并不占布局位置,占用资源非常小的视图对象==。
4.使用ViewStub注意的点:
- ViewStub==只能加载一次==,之后ViewStub对象会被置空。也就是==布局被加载后就不能再用ViewStub来控制它的显示隐藏==。
- ViewStub==只能用来加载一个布局文件,而不是某个具体的View==。
- ViewStub==不能嵌套Merge标签==。
- ViewStub主要使用场景:
- 在程序运行期间,某个布局被加载后,状态就不会有变化。
- 想要控制一个布局文件的隐藏/显示,而不是某个view
使用
在布局文件中
<ViewStub
android:id="@+id/view_stub_id"
android:layout="@layout/view_stub"
android:inflatedId="@+id/view_stub_id"
android:layout_width="200dp"
android:layout_height="50dp" />
由于ViewStub是直接继承自View的,所以它在xml里的基本使用方法和其他控件是一样的。
在xml中定义ViewStub后就可以在代码里直接使用并根据具体业务逻辑在需要显示的时候对其进行加载:
ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub_id);
if (viewStub != null) {
// 调用 inflate 加载真正的布局资源并返回创建的 View 对象
View inflatedView = viewStub.inflate();
// 在得到真正的 View 对象后,就可以和直接加载的控件一样使用了。
TextView textView = inflatedView.findViewById(R.id.view_stub_textview);
}
原理分析
/**
* A ViewStub is an invisible, zero-sized View that can be used to lazily inflate
* layout resources at runtime.
*
* When a ViewStub is made visible, or when {@link #inflate()} is invoked, the layout resource
* is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views.
* Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or
* {@link #inflate()} is invoked.
...
*/
@RemoteView
public final class ViewStub extends View {
}
从这段介绍中可以知道==ViewStub是一个不可见并且大小为0的控件==,其作用就是用来实现布局资源的“懒加载”,==当调用setVisibility()或者inflate()时,和ViewStub相关的布局资源就会被加载并在控件层级结构中替代ViewStub,同时ViewStub会从控件层级结构中移除,不再存在==。
初始化过程
public final class ViewStub extends View {
// 在 xml 中定义的 android:inflatedId 值,用于加载后的 View Id。
private int mInflatedId;
// 在 xml 中定义的 android:layout 值,是需要真正加载的布局资源。
private int mLayoutResource;
// 保存布局创建的 View 弱引用,方便在 setVisibility() 函数中使用。
private WeakReference<View> mInflatedViewRef;
// 布局加载器
private LayoutInflater mInflater;
// 布局加载回调接口,默认为空。
private OnInflateListener mInflateListener;
public ViewStub(Context context) {
this(context, 0);
}
/**
* Creates a new ViewStub with the specified layout resource.
*
* @param context The application's environment.
* @param layoutResource The reference to a layout resource that will be inflated.
*/
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);
// 获取 xml 中定义的 android:inflatedId 和 android:layout 属性值
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
// 获取 ViewStub 在 xml 中定义的 id 值
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
// 设置 ViewStub 控件的显示属性,直接设置为不显示。
setVisibility(GONE);
// 设置 ViewStub 不进行绘制
setWillNotDraw(true);
}
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置宽高都为 0
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
// 空方法,不进行任何绘制。
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
}
从上面的代码可以看出:
- ==ViewStub的不可见性==:在ViewStub的构造函数中,利用setVisibility(GONE)将可见性设置为不可见,所以无论在 xml里如何设置,都是不可见的。
- ==ViewStub的不绘制性==:在 ViewStub的构造函中,利用setWillNotDraw(true)使其不进行绘制并且把draw()实现为空方法,这些都保证了ViewStub在加载的时候并不会进行实际的绘制工作。
- ==ViewStub的零大小性==:在onMeasure()中把宽高都直接指定为0,保证了其大小为0。
懒加载过程
==在调用inflate()或者setVisibility()时,ViewStub才会加载真正的布局资源并在控件层级结构中替换为真正的控件,同时ViewStub从控件层级结构中移除,这是“懒加载”的核心思想==。
public View inflate() {
// 获取 ViewStub 在控件层级结构中的父控件。
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
// 根据 android:layout 指定的 mLayoutResource 加载真正的布局资源,渲染成 View 对象。
final View view = inflateViewNoAdd(parent);
// 在控件层级结构中把 ViewStub 替换为新创建的 View 对象。
replaceSelfWithView(view, parent);
// 保存 View 对象的弱引用,方便其他地方使用。
mInflatedViewRef = new WeakReference<>(view);
// 渲染回调,默认不存在。
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
// 返回新创建的 View 对象
return view;
} else {
// 如果没有在 xml 指定 android:layout 会走到这个路径,所以 android:layout 是必须指定的。
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
// 在第一次调用 inflate() 后,ViewStub 会从控件层级结构中移除,不再有父控件,
// 此后再调用 inflate() 会走到这个路径,所以 inflate() 只能调用一次。
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
private View inflateViewNoAdd(ViewGroup parent) {
// 获取布局渲染器
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
// 把真正需要加载的布局资源渲染成 View 对象。
final View view = factory.inflate(mLayoutResource, parent, false);
// 如果在 xml 中指定 android:inflatedId 就设置到新创建的 View 对象中,可以不指定。
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
// 把 ViewStub 从控件层级中移除。
parent.removeViewInLayout(this);
// 把新创建的 View 对象加入控件层级结构中,并且位于 ViewStub 的位置,
// 并且在这个过程中,会使用 ViewStub 的布局参数,例如宽高等。
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
可以看到,==加载时会把真正需要加载的布局资源渲染成 View 对象,然后把 ViewStub 从控件层级中移除。再把新创建的 View 对象加入控件层级结构中,并且位于 ViewStub 的位置==,并且在这个过程中,会使用 ViewStub 的布局参数,例如宽高等。
再来看下另一种显示方式setVisibility()函数代码:
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
// 如果已经调用过 inflate() 函数,mInflatedViewRef 会保存新创建 View 对象的弱引用,
// 此时直接更新其可见性即可。
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
// 如果没有调用过 inflate() 函数就会走到这个路径,会在设置可见性后直接调用 inflate() 函数。
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}
setVisibility()的代码逻辑也很简单:如果inflate()已经被调用过就直接更新控件可见性,否则更新可见性并调用inflate()加载真正的布局资源,渲染成 View 对象。