美文网首页
ViewStub的使用和源码分析

ViewStub的使用和源码分析

作者: 风月寒 | 来源:发表于2020-10-27 12:47 被阅读0次
    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);
    }
    

    相关文章

      网友评论

          本文标题:ViewStub的使用和源码分析

          本文链接:https://www.haomeiwen.com/subject/hcxxvktx.html