为了优化UI加载,通常会把不需要立即显示的View放到ViewStub里,在需要的时候按需加载,以此来优化UI性能。
- 特点
1.ViewStub 是一个轻量级的View,没有尺寸,不绘制任何东西
2.在视图树中充当占位符的作用,在需要的时候才加载真正显示的View,实现View的延迟加载,避免资源浪费,减少渲染时间。
3.缺点是ViewStub所要替代的layout根布局是<merge>标签
- 基本用法:
1.在布局中直接引用ViewStub, 通过ViewStub的属性来指定对应的layout即可,如下:
<ViewStub
android:id="@+id/viewstub"
android:layout_width="552dp"
android:layout_height="wrap_content"
android:layout_marginStart="65dp"
android:layout_marginTop="570dp"
android:layout="@layout/loading_layout" />
2.使用时,通过调用ViewStub的setVisibility()或者inflate()方法,实现加载显示。两者的区别,后面结合源码分析
3.注意事项:a).layout只能加载一次 b)layout加载之后,就不能通过ViewStub的Id获取到了。
- 源码分析
1.ViewStub在布局中起到占位符的作用,本身不显示,不绘制,如下。
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
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();
//设置ViewStub不可见,不绘制
setVisibility(GONE);
setWillNotDraw(true);
}
2 . 加载指定的layout布局并添加到ViewStub的Parent中,加载成功之后ViewStub会从它的父容器,因此无法再使用。
a) 通过inflate()方法加载layout,inflate会通过LayoutInflater加载对应资源id对应的布局文件,
/**
* Inflates the layout resource identified by {@link #getLayoutResource()}
* and replaces this StubbedView in its parent by the inflated layout resource.
*
* @return The inflated layout resource.
*
*/
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
//mLayoutResource 真正要加载的布局文件
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view = inflateViewNoAdd(parent);
//使用真正要加载的布局替换viewStub
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 {
// 这里也说明了为什么ViewStub不能放在merge标签下,因为merge不是View,
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
加载获取对应的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;
}
替换并移除ViewStub, 加载真正要显示的布局后,ViewStub便从父容器里移除掉了,也从整个视图树中移除了,所findViewById找不到。
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);
}
}
b) 通过setVisibility()方法加载,在已经加载过View的情况下,会从缓存中获取显示View,在没加载时会调用inflate()方法,也就是上面的加载流程。
/**
* When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
* {@link #inflate()} is invoked and this StubbedView is replaced in its parent
* by the inflated layout resource. After that calls to this function are passed
* through to the inflated view.
*
* @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
*
* @see #inflate()
*/
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
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();
}
}
}
c) ViewStub为什么不支持merge ?
首先,了解一下merge标签的特点:引自https://www.jianshu.com/p/69e1a3743960
- merge必须放在布局文件的根节点上。
- merge并不是一个ViewGroup,也不是一个View,它相当于声明了一些视图,等待被添加。
- merge标签被添加到A容器下,那么merge下的所有视图将被添加到A容器下。
- 因为merge标签并不是View,所以在通过LayoutInflate.inflate方法渲染的时候, 第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。
- 因为merge不是View,所以对merge标签设置的所有属性都是无效的。
主要关注红色加深处,merge载通过LayoutInflate.inflate方法渲染时必须指定父容器,切attachToRoot必须为true。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
回到ViewStub加载View的方法,如下,可以看到,加载布局的attachToRoot参数默认值为false,因此ViewStub不支持merge,当我们使用merge时会报错:Caused by: android.view.InflateException: can be used only with a valid ViewGroup root and attachToRoot=true 。
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;
}
- 典型应用
Viewpager+Fragment的形式,在有多个Fragment的情况下,由于想要进行预加载,在首次进入某一个页面时,虽然只有一个Fragment呈现在眼前,Viewpager下其他的Fragment的生命周期函数onCreateView(), onResume()也会执行(与setOffscreenPageLimit的设置有关),也就是说其他Fragment页面布局的加载,在onCreateView(), onResume()里的逻辑也会被执行,这显然不是很有必要。而且会增加我们想要打开的界面的加载时长。
此时就可以通过:ViewStub+setUserVisibleHint()实现布局的懒加载以及延迟初始化,在onCreateView中只加载布局的"壳",在真正切换到要显示的fragment页时,再将真正的布局加载进来。
主要代码如下:
/**
* 标志位,标志已经初始化完成
*/
private boolean isPrepared;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.joy_listen_fragment, container, false);
viewStub = view.findViewById(R.id.enjoy_viewstub);
...
isPrepared = true;
return view;
}
public void initVew(View view) {
......
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isPrepared && isVisibleToUser) {
if (viewStub.getParent() != null) {
View view = viewStub.inflate();
initVew(view);
mAppsPresenterImpl = new AppsPresenterImpl(this, getActivity());
mAppsPresenterImpl.getCacheApps(DBConstants.APPS_TYPE_JOY_LISTEN, null);
.....
}
}
}