ViewStub使用和源码分析

579 阅读4分钟
ViewStub的使用
xml文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <LinearLayout android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:id="@+id/btn_vs_showView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="显示ViewStub"/>
        <Button
            android:id="@+id/btn_vs_changeHint"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="更改ViewStub"/>
        <Button
            android:id="@+id/btn_vs_hideView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_weight="1"
            android:text="隐藏ViewStub"/>
    </LinearLayout>

    <!--ViewStub 展示或者隐藏内容-->
    <ViewStub
        android:id="@+id/viewstub_test"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:inflatedId="@+id/iv_VsContent"
        android:layout="@layout/iv_vs_content"/>

</RelativeLayout>

可以看到,ViewStub必须添加layout,这个是它需要展示的东西。inflatedId则可以加也可以不加,并不影响显示。它的作用是inflateId 表示给被引用/填充的 layout 资源设置一个id,通过它可以获取到被引用/填充的 layout 的 View 实例。

然后显示的时候可以调用inflate()得到一个view.

View view = viewstub_test.inflate();

隐藏的时候调用setVisibility()

viewstub_test.setVisibility(View.INVISIBLE);
源码分析
ViewStub的创建
public final class ViewStub extends View{}

ViewStub是集成于View的,所以它的创建也是调用createViewFromTag(),createViewFromTag有多个重载方法,最终调用的是

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
        ......
        try {
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } 
    }

主要看下面的代码

if (-1 == name.indexOf('.')) {
    view = onCreateView(context, parent, name, attrs);
} else {
    view = createView(context, name, null, attrs);
}

根据标签里面有没有.来区分是自定义的控件,还是系统自带的控件

<TextView
        android:id="@+id/tvNum"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="200dp"
        android:layout_marginLeft="50dp"
        android:text="18 cm"
        android:textColor="#ffffffff"
        android:textSize="42sp"
        android:textStyle="bold"
        />
    <com.example.customview.ruler.RulerView
        android:id="@+id/ruler_view"
        android:layout_centerInParent="true"
        android:layout_width="match_parent"
        android:layout_height="200dp"/>

从上面可以知道,系统的控件在使用的时候直接写这个控件就好,而自定义的控件则需要写全路径。

@Nullable
    public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                                mContext.getClassLoader()).asSubclass(View.class);

                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, viewContext, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
            }

            try {
                final View view = constructor.newInstance(args);
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
    }

在createView()主要做了以下几件事。

1、从map中获取构造函数对象,如果不存在,则通过反射的方式创建一个实例,并保存在map中。

2、创建好构造函数实例之后调用newInstance(),创建一个view.如果view属于ViewStub,则返回去。此时ViewStub则创建好了。

ViewStub的构造函数
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context);

        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.ViewStub, defStyleAttr, defStyleRes);
        saveAttributeDataForStyleable(context, R.styleable.ViewStub, attrs, a, 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构造函数中,setVisibility(GONE)表示将视图设置为不可见,并且不会去绘制。这也是在布局优化的时候会使用ViewStub.

setVisibility()
public void setVisibility(int visibility) {
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }

当我们第一次进去的时候mInflatedViewRef是为null的,则会走else逻辑。

由于我们在构造方法中setVisibility(GONE),则不会走inflate(),这样就不会显示ViewStub所指定的layout资源。那如果想要加载ViewStub所指定的layout资源,需要设置ViewStub控件设置可见,或者调用inflate()方法。

特别注意:

setVisibility(int visibility)方法,参数visibility对应三个值分别是INVISIBLE、VISIBLE、GONE

VISIBLE:视图可见

INVISIBLE:视图不可见的,它仍然占用布局的空间

GONE:视图不可见,它不占用布局的空间

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");
        }
    }

在inflate()主要做了以下几件事:

1、调用inflateViewNoAdd方法返回android:layout指定的布局文件最顶层的view

2、调用replaceSelfWithView方法, 移除ViewStub, 添加view到被移除的ViewStub的位置

3、初始化mInflatedViewRef,添加view到 mInflatedViewRef 中

4、加载完成之后,回调onInflate 方法

这样第二次进来的时候,在setVisibility()就会走if的逻辑,通过setVISIBLE或者INVISIBLE来显示和不显示。

ViewStub的注意事项

1、使用ViewStub需要在xml中设置android:layout,不是layout,否则会抛出异常

throw new IllegalArgumentException("ViewStub must have a valid layoutResource");

2、ViewStub不能作为根布局,它需要放在ViewGroup中, 否则会抛出异常

throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");

3、一旦调用setVisibility(View.VISIBLE)或者inflate()方法之后,该ViewStub将会从试图中被移除(此时调用findViewById()是找不到该ViewStub对象).

// 获取ViewStub在视图中的位置
final int index = parent.indexOfChild(this);
// 移除ViewStub
// 注意:调用removeViewInLayout方法之后,调用findViewById()是找不到该ViewStub对象
parent.removeViewInLayout(this);

4、如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId

// mInflatedId 是在xml设置的 inflateId
if (mInflatedId != NO_ID) {
    // 将id复制给view
    view.setId(mInflatedId);
    //注意:如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
}

5、被inflate的layoutView的layoutParams与ViewStub的layoutParams相同.

final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 将xml中指定的 android:layout 布局文件中最顶层的View 也就是根view,
// 添加到被移除的 ViewStub的位置
if (layoutParams != null) {
    parent.addView(view, index, layoutParams);
} else {
    parent.addView(view, index);
}