我们知道布局优化有三个标签,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,则会执行抛出异常的代码逻辑,这点要注意。