谈到布局优化,通常都会想到标签include和merge。简单先说下这两个标签的好处:include可以减少布局文件内容,比如,在我们需要在多个布局中都添加标题栏时,可以创建一个单独的xml文件,添加标题内容到该xml中,然后在需要用到的目标布局里面用include标签添加已创建好的标题栏;merge可以减少多余的包含控件。两者一起使用,可以减少布局的层级结构,从而减少绘制的工作量。注意:include标签只支持android:layout_with和android:layout_height属性,其它属性是不支持的。O了~开始这章的重点,
很多时候,在初始化程序时都是一次性把布局加载进来,使得绘制工作量变多,导致程序初始化性能降低。比如:网络请求错误时的布局、一些评论显示的布局(未有评论前)等等,这些布局我们并不经常用到,能不能按需加载?可以,那就是ViewStub。
1、为什么用ViewStub能提升程序初始化性能
ViewSub继承于View,本身是一个视图。但其有一个特点,就是轻量级的,宽和高都为0,并且它不参与绘制和任何的布局,为什么这么说,一起看下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); //在初始构造方法中就已经设置了GONE
setWillNotDraw(true);
}
... ...//省略部分代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0); //测量中设置宽和高为0
}
@Override
public void draw(Canvas canvas) {
//无任何绘制
}
根据上面的代码,我们可以知道,声明一个ViewStub时,只是起到了一个占位的作用,并没有实际的绘制和布局。
2、怎么去使用ViewStub,做到按需加载,一起看下下面一段简单描述。
首先,主布局声明一个ViewStub:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.rickybin.viewstubtest.MainActivity">
<ViewStub
android:id="@+id/textStub"
android:inflatedId="@+id/text_inflate_id" //设置目标布局ID
android:layout="@layout/text_layout" //获取目标布局
android:layout_width="match_parent"
android:layout_height="wrap_content" />
RelativeLayout>
text_layout的内容:
"http://schemas.android.com/apk/res/android"
android:id="@+id/textId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ViewStub的目标TextView" />
activity的代码:
public class MainActivity extends AppCompatActivity {
private ViewStub textStub;
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
textStub = (ViewStub) findViewById(R.id.textStub);
if (mTextView != null){
mTextView = (TextView) textStub.inflate(); //加载相应的TextView
}
mTextView.setText("获取textview");
}
}
在activity代码initView()中判断mTextView是否为null,调用inflate()加载,这时就会把“@layout/text_layout”实例化出来。ViewStub还有一种方法加载布局,就是用setVisibility去加载,其实这个方法中最终还是调用了inflate()加载布局。再看看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);//先由父类setVisibility再inflate
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}
既然都是用inflate()加载,那我们一起看看在inflate里具体做了什么:
public View inflate() {
final ViewParent viewParent = getParent(); //获取ViewStub所在的父视图
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view = inflateViewNoAdd(parent);
replaceSelfWithView(view, parent); //替换ViewStub自身
//把View赋值给mInflatedViewRef
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指定的布局,赋值给变量View
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;
}
replaceSelfWithView()方法
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
//移除父布局中的ViewStub
parent.removeViewInLayout(this);
//这里是用ViewStub的android:with和android:height设置目标布局
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
//添加目标布局到父布局中
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
从上面我们可以分析得到结果,ViewStub去加载目标布局时,其实是先把LayoutInflater指定的layout实例化后赋值给变量View,然后ViewStub自身会从父视图中移除,再把变量View添加进父视图中。也就是说ViewStub只能inflate一次,之后ViewStub对象就会置空,不再是整个布局结构中的一部分了。要注意一点,如果ViewStub设置了inflatedId,将会赋值给变量View的id。