ViewStub 使用及源码解析

我们知道布局优化有三个标签,include 、merge 和 ViewStub。 我们可以把公共的布局抽取到一个 xml 中,然后使用 include 来引用; 布局会分层次,如果里层和上一层是同样的容器,则可以使用 merge,但记住一点,merge 一定是在根节点; ViewStub 不是关键字,而是 View 的一个子类,它的作用是占坑,延迟加载它里面引用的布局。详细说说 ViewStub 这个类,先看个例子,看看它是怎么用的
 

layout_content:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <ViewStub
        android:id="@+id/game_over_id"
        android:layout="@layout/layout_game_over"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>

layout_game_over:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#CC000000"
    android:id="@+id/fl_pc_view"
    >

    <ImageView
        android:id="@+id/iv_pc_close"
        android:layout_width="22dp"
        android:layout_height="22dp"
        android:layout_gravity="right|top"
        android:layout_margin="20dp"
        android:background="@drawable/pc_close" />

    <TextView
        android:id="@+id/tv_pc_desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="破产说明"
        />

</FrameLayout>

Activity:

    private void initVS() {
        ViewStub vs = findViewById(R.id.game_over_id);
        View view = vs.inflate();
        ImageView imageView = view.findViewById(R.id.fl_pc_view);
        TextView textView = view.findViewById(R.id.tv_pc_desc);
    }

注意,在 Activity 中,我们可以在 setContentView(R.layout.layout_content); 之后的任意时间里调用 initVS() 方法,这个根据业务的需求来判断,调用 initVS() 后,layout_game_over 布局才会被加载到 layout_content 中,这样做的好处就是在 layout_content 布局初始化的时候,里面创建和绘制的view会比较少,这样可以提高初始化的效率。我们看看 ViewStub 的代码及实现原理

attrs:
    <declare-styleable name="ViewStub">
        <attr name="id" />
        <attr name="layout" format="reference" />
        <attr name="inflatedId" format="reference" />
    </declare-styleable>

这个是自定义的属性,写过自定义控件的都应该了解。 看看 ViewStub 的构造方法,最终都会调用这个构造

    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context);

        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.ViewStub, 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);
    }

在这里要注意两点:一是调用的 super 方法,是一个参数的构造,也就是说父类 View 中不会执行读取自定义属性的方法,因此即使你在 xml 中 ViewStub 的节点下写了 View 通用的属性,也不会去读取,更不会起作用。同时,mID 对应xml中的 id,也不会被获取到,因此在 ViewStub 构造中,在这里重新读取自定义属性的值后,给 mID 赋值;二是通过属性读取了在xml中引用的 layout 布局的id值,赋值给成员变量,以备使用;最后两行代码是把这个控件消失,并且不让绘制,看到这,顺便再看看相关的其它几个方法

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }

我们知道控件是需要测量和绘制,这里直接把大小强制改为0,并且重写了 draw() 和 dispatchDraw() 方法,仔细看,去掉了 super() 方法,父类的 draw(Canvas canvas) 方法还回去绘制一些背景,这里直接把它也给省略了,避免了不必要的性能浪费。


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

先不看外面的判断,直接看里面的方法, mLayoutResource 是必不可少的,因为要通过它来转换布局;通过 getParent() 方法获取到当前 ViewStub 控件的父容器;看看 inflateViewNoAdd(parent)方法,看名字的意思也就大概猜出来它的功能,看代码

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

        if (mInflatedId != NO_ID) {
            view.setId(mInflatedId);
        }
        return view;
    }

如果我们自己设置的有 mInflater,则用自己的,否则使用 LayoutInflater,通过 inflate(mLayoutResource, parent, false) 方法来转化layout布局为 View,注意第三个形参,是false,也就是说不会添加到 parent 中;最下面是设置 id;这个方法的作用就是把 mLayoutResource 对应的 layout 布局转化为一个单独的 View 控件,看看下个方法 replaceSelfWithView(view, parent)

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

这个方法中,找到当前 ViewStub 在父容器中的索引位置,然后把自己从父容器中移除;view 是 mLayoutResource 对应的layout布局转换的 View,根据 index 值把它添加到父容器中指定的位置,如果获取自己的 LayoutParams 属性不为 null,把它也赋值给 view。看到这里,我们就明白了, ViewStub 只是占了个坑,是个空壳公司,当调用 inflate() 方法时,转换布局,然后把自己的位置让给需要的控件。 继续看 inflate() 方法,替换view后,会把它加入 mInflatedViewRef 软引用中,同时调用 OnInflateListener 这个监听回调。此时再看外层的判断,发现 inflate() 只能被调用一次,如果调用第二次,由于 ViewStub 已经被父容器移除, ViewParent viewParent = getParent() 会为 null,则会抛异常。 至于 setVisibility(int visibility) 方法,则可以被多次调用,其中的逻辑大家可以自己看看。

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

源码读了一遍,就知道 merge 和 ViewStub 为何不能同时使用了,因为 LayoutInflater 的 inflate() 方法

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            View result = root;
            try {
                ...
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    ...
                }

            } catch (XmlPullParserException e) {
                ...
            }

            return result;
        }
    }

如果使用了 merge 标签,此时方法中 attachToRoot 为 false,则会执行抛出异常的代码逻辑,这点要注意。
 

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/104782366