ViewStub

383 阅读3分钟

构造过程

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方法。

总结:

  1. ViewStub的不可见性:在ViewStub的构造函数中,利用setVisibility(GONE)将可见性设置为不可见,所以无论在 xml里如何设置,都是不可见的。

  2. ViewStub的不绘制性:在 ViewStub的构造函中,利用setWillNotDraw(true)使其不进行绘制并且把draw()实现为空方法,这些都保证了ViewStub在加载的时候并不会进行实际的绘制工作。

  3. ViewStub的零大小性:在onMeasure()中把宽高都直接指定为0,保证了其大小为0

正是由于ViewStub的这些特性,其加载过程几乎不需要时间,可以认为它的存在不会对相关程序的启动产生影响。

懒加载过程: 就是把ViewStub移除,然后把layout的View添加到ViewStub的父View。

参考: