Android源码系列(24) -- ViewStub

June 26, 2019

虽然 ViewStub 父类是 View,但其本质是 不可见无尺寸懒加载 的布局填充工具,自身没有尺寸和绘图逻辑,不能展示在界面上。

1
public final class ViewStub extends View

ViewStub 被设置为可见或调用 inflate() 时进行布局填充。填充过程 ViewStub 从所在父布局中移除,并把懒加载填充的布局添加到相应位置,新填充布局沿用预设给 ViewStubLayoutParams

类注释提供以下示例:

1
2
3
4
5
<ViewStub android:id="@+id/stub"
      android:inflatedId="@+id/subTree"
      android:layout="@layout/mySubTree"
      android:layout_width="120dip"
      android:layout_height="40dip" />

示例定义 ViewStub 的id为 R.id.stub,填充布局资源为 R.layout.mySubTree,并指定 R.id.subTree 为新视图id,这样就能用这个id引用填充后的视图。由于 ViewStub 加载布局后就被移除,所以再次调用并填充布局会出现异常。

1
2
3
4
5
6
// 填充之后给新视图设置的id
private int mInflatedId;
// 需要填充布局的id
private int mLayoutResource;

private WeakReference<View> mInflatedViewRef;

有多个重载构造方法,但最终都会来到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context);

    // 从xml获取参数值
    final TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.ViewStub, defStyleAttr, defStyleRes);
    // 获取填充之后要给新视图设置的id
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    // 获取需要填充布局的id
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
    mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
    a.recycle();

    // 因为ViewStub不具备UI功能,所以要设置为不可见
    setVisibility(GONE);
    // 没有UI也不需要绘制
    setWillNotDraw(true);
}

调用以下方法就能懒加载布局,和调用 ViewStubsetVisibility() 的作用一样。

填充时检查父布局合法性,若父布局为空存在两种情况:

  • ViewStub 通过代码构建,但没添加到父布局中;
  • ViewStub 曾加载过,已失效并从父布局移除;

这两种情况都不允许 ViewStub 再次填充,所以要抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public View inflate() {
    // 获取ViewStub所在父布局
    final ViewParent viewParent = getParent();

    // 检查父布局不为空且必须为ViewGroup,只有ViewGroup才能存放子视图
    if (viewParent != null && viewParent instanceof ViewGroup) {
        // 检查是否指定要加载布局的资源id
        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");
    }
}

懒加载视图经过 LayoutInflater 填充为实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);

    // 如果额外指定id,就把指定的id设置给新视图
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    return view;
}

获得填充后的布局实例,还要处理 ViewStub 本身,前后共四个步骤:

  • 获取 ViewStub 在父布局的位置索引;
  • ViewStub 从所在父布局中移除;
  • ViewStubLayoutParams 提供给新视图;
  • 新视图放在父布局 ViewStub 原位置,即第一步获取的索引索引;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void replaceSelfWithView(View view, ViewGroup parent) {
    // 获取ViewStub在父布局的位置
    final int index = parent.indexOfChild(this);

    // ViewStub从所在父布局中移除,结束占位
    parent.removeViewInLayout(this);

    // 把ViewStub的LayoutParams提供给新视图
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();

    // 新视图放在父布局ViewStub原来的位置
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
        parent.addView(view, index);
    }
}

LayoutInflater 实现里面,遇到 ViewStub 只是创建新实例,然后把当前使用的 LayoutInflater 实例提供给 ViewStub,以后加载视图时使用该 LayoutInflater

1
2
3
4
5
6
7
8
9
10
11
// 创建类实例,args是自定义主题相关变量
final View view = constructor.newInstance(args);

// 类型是ViewStub
if (view instanceof ViewStub) {
    // 使用相同Context给ViewStub设置LayoutInflater
    final ViewStub viewStub = (ViewStub) view;
    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;

通过以下代码检查 ViewStub 是否已加载布局:

1
2
3
4
5
if (mViewStub.getParent() != null) {
    mViewStub.inflate();
} else {
    mViewStub.setVisibility(View.VISIBLE);
}

参考链接:

  • https://stackoverflow.com/questions/23783101/how-to-check-if-a-viewstub-is-already-inflated