性能优化之布局优化篇一 使用ViewStub视图

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/ruancoder/article/details/52416405
一、概述
ViewStub是一个不显示且不占用布局空间的视图。ViewStub需要指定一个布局layout,在ViewStub初始化时,其指定的layout并未初始化。只有当ViewStub的setVisibility(VISIBLE/INVISIBLE)或inflate()方法被调用,ViewStub所指向的布局layout才会实例化,该layout会使用ViewStub的布局参数LayoutParam,被添加到ViewStub的父视图中来替代ViewStub,即ViewStub将不在布局中存在。

ViewStub类官方文档:
https://developer.android.com/reference/android/view/ViewStub.html

二、使用场景
在开发中,会遇到这样的情况,需要在运行时动态的决定显示某个视图。通常的做法是,将可能会用到的所有视图都添加到布局中,之后在代码中通过View.VISIBILITY和View.GONE来改变视图的可见性。即使在xml布局中为视图设置了android:visibility="gone",该视图仍然会被实例化,这样做带来的缺点是增加了视图加载的时间和内存的消耗。

ViewStub的一个典型的使用案例,当我们启动一个页面,如果请求到数据就显示内容视图,遇到网络出错时显示网络异常视图。

添加网络异常视图net_error_view.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center"
              android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/net_icon"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="网络异常\n点击屏幕重新加载"
        android:textColor="#666666"
        android:textSize="16sp"/>
</LinearLayout>

Activity的布局分两部分,一个内容视图,一个网络异常视图。网络异常视图使用ViewStub,并将layout指定为上面的net_error_view。
<?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="#ffffff">

    <!-- 内容视图 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    </LinearLayout>

    <!-- 网络异常视图 -->
    <ViewStub
        android:id="@+id/viewstub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inflatedId="@+id/inflatedview"
        android:layout="@layout/net_error_view"/>
</FrameLayout>

在Activity中,如果需要显示网络异常视图,使用如下代码。
private View netErrorView;

ViewStub viewStub = (ViewStub) findViewById(R.id.net_error);
netErrorView = viewStub.inflate();

viewStub.setVisibility(View.VISIBLE);
netErrorView = findViewById(R.id.inflatedview);


当ViewStub的inflate()方法被调用时,其指定的@layout/net_error_view被实例化并显示出来。此时,ViewStub的android:inflatedId即inflatedview,就成为net_error_view视图根布局的id。这里我们将网络异常视图对象保存到成员变量netErrorView中,之后可以直接操作netErrorView,不用再调用findViewById()。


三、源码分析

ViewStub继承自View类,所以本质上还是一个View。
public final class ViewStub extends View {
}

ViewStub视图在加载时,为何没有显示,且指定的layout没有被初始化,原因在于ViewStub重写了View类的onMeasure()和draw()方法。在onMeasure()方法中,调用setMeasuredDimension,传递的参数measuredWidth和measuredHeight都是0。draw()方法的内部实现被置空,也就不会绘制了。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(0, 0);
}

@Override
public void draw(Canvas canvas) {
}

ViewStub除了拥有父类View的属性外,新增了android:inflatedId和android:layout,这两个属性在构造方法中完成初始化。
private int mInflatedId;
private int mLayoutResource;

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);
}
获取到属性inflatedId和layout的值后分别赋值给成员变量mInflatedId和mLayoutResource。并在最后调用了setWillNotDraw(true),只有View就不会绘制自己。

当ViewStub的inflate()方法被调用时,指定的layout被实例化显示出来,并返回。我们来看看inflate()方法的实现。
public View inflate() {
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
	    // 获取ViewStub的父视图,强转为ViewGroup类型
            final ViewGroup parent = (ViewGroup) viewParent;
	    // 获取LayoutInflater对象
            final LayoutInflater factory;
            if (mInflater != null) {
                factory = mInflater;
            } else {
                factory = LayoutInflater.from(mContext);
            }
	    // 实例化mLayoutResource,赋值给view变量
            final View view = factory.inflate(mLayoutResource, parent,
                    false);

            // 如果设置了mInflatedId,赋值给view的id
            if (mInflatedId != NO_ID) {
                view.setId(mInflatedId);
            }

            // 移除ViewStub自身
            final int index = parent.indexOfChild(this);
            parent.removeViewInLayout(this);

            // getLayoutParams()获取的是ViewStub的布局参数
            final ViewGroup.LayoutParams layoutParams = getLayoutParams();
	    // 把view添加到ViewStub的父视图中
            if (layoutParams != null) {
                parent.addView(view, index, layoutParams);
            } else {
                parent.addView(view, index);
            }

            // 把view赋值给成员变量mInflatedViewRef
            mInflatedViewRef = new WeakReference<View>(view);

            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }

            // 最后返回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将指定的layout实例化赋值给变量view。如果设置了inflatedId,赋值给该view的id属性。然后,从ViewStub的父视图parent中移除ViewStub自身,使用ViewStub的布局参数把view添加到parent中。最后,把view赋值给成员变量mInflatedViewRef,返回view。

ViewStub的setVisibility()方法被调用时,指定的layout同样也可以被实例化。ViewStub对View中的setVisibility()方法进行了重写。
@Override
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {// 如果ViewStub的inflate()已经被调用,执行if
        View view = mInflatedViewRef.get();
	// 此时ViewStub已经被inflate(),view已经有值,直接执行view的setVisibility()方法
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {// 如果ViewStub的inflate()还未被调用,执行else
        // 先调用父类View的setVisibility()
        super.setVisibility(visibility);
	// 如果传入的参数为VISIBLE或INVISIBLE,执行inflate()方法
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}
在setVisibility()方法被调用之前,如果ViewStub的inflate()已经调用,此时成员变量mInflatedViewRef即是ViewStub指定的layout对象,直接执行view的setVisibility()方法。如果ViewStub的inflate()未被调用,先将参数传递到父类View的setVisibility(),然后判断传入的参数为VISIBLE或INVISIBLE时,执行inflate()方法,去实例化layout对象。

所以,当ViewStub的inflate()方法被调用,或setVisibility()方法被调用并传入参数VISIBLE或INVISIBLE,ViewStub所指向的布局layout会被实例化,并添加到ViewStub的父视图中显示出来。
阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页