构造过程
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
// 要被加载的布局 Id
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
// 要被加载的布局
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
// ViewStub 的 Id
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
// 初始状态为 GONE
setVisibility(GONE);
// 设置为不会绘制
setWillNotDraw(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
懒加载过程
// 复写了 setVisibility(int) 方法
@Override
@android.view.RemotableViewMethod
public void setVisibility(int visibility) {
// private WeakReference<View> mInflatedViewRef;
// mInflatedViewRef 是对布局的弱引用
if (mInflatedViewRef != null) {
// 如果不为 null,就拿到懒加载的 View
View view = mInflatedViewRef.get();
if (view != null) {
// 然后就直接对 View 进行 setVisibility 操作
view.setVisibility(visibility);
} else {
// 如果为 null,就抛出异常
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
// 之前说过,setVisibility(int) 也可以进行加载布局
if (visibility == VISIBLE || visibility == INVISIBLE) {
// 因为在这里调用了 inflate()
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() {
// 获取 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);
}
}
可以多次inflate()吗
在第一次调用 inflate() 后,ViewStub 会从控件层级结构中移除,不再有父控件。具体查看inflate方法。
总结:
-
ViewStub的不可见性:在ViewStub的构造函数中,利用setVisibility(GONE)将可见性设置为不可见,所以无论在xml里如何设置,都是不可见的。 -
ViewStub的不绘制性:在ViewStub的构造函中,利用setWillNotDraw(true)使其不进行绘制并且把draw()实现为空方法,这些都保证了ViewStub在加载的时候并不会进行实际的绘制工作。 -
ViewStub的零大小性:在onMeasure()中把宽高都直接指定为0,保证了其大小为0
正是由于ViewStub的这些特性,其加载过程几乎不需要时间,可以认为它的存在不会对相关程序的启动产生影响。
懒加载过程: 就是把ViewStub移除,然后把layout的View添加到ViewStub的父View。