為了優化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);
.....
}
}
}