ViewStub的使用
簡介
ViewStub
是一種沒有任何維度的輕量型視圖,它不會繪製任何內容或參與佈局。
- ViewStub是一種沒有大小,不佔用佈局的View。
- 直到當調用
inflate()
方法或者可見性變爲VISIBLE時,纔會將指定的佈局加載到父佈局中。 - ViewStub加載完指定佈局之後會被移除,不再佔用空間。(所以
inflate()
方法只能調用一次 )
因爲這些特性ViewStub可以用來懶加載佈局,優化UI性能。
使用
佈局
在佈局中添加ViewStub標籤並通過layout屬性指定要替換的佈局。
<ViewStub
android:id="@+id/visible_view_stub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/layout_view_stub_content" />
代碼
在需要展示佈局的地方調用 inflate()
方法或者將ViewStub的可見性設置爲VISIBLE。
private View viewStubContentView = null;
visibleViewStub.setVisibility(View.VISIBLE);
if(viewStubContentView == null){
viewStubContentView = inflateViewStub.inflate();
}
注意 :inflate()
方法只能調用一次,重複調用被拋出IllegalStateException
異常。
inflate()
方法會返回替換的佈局的根View而設置VISIBLE不會返回,如果需要獲取替換佈局的實例,如:需要爲替換的佈局設置監聽事件,這是需要使用inflate()
方法而不是VISIBLE。
ViewStub源碼分析
針對我們前面說的ViewStub的幾個特點,我們來分析下源碼是如何實現的。分析源碼可以學習別人優秀的代碼設計,也可以爲我們日後類似需求的實現提供借鑑。
ViewSutb沒有大小,不佔用佈局
ViewStub在構造方法中設置了控件可見性爲GONE並且指定不進行繪製。
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);
}
並且重寫了onMeasure(widthMeasureSpec, heightMeasureSpec)
設置尺寸爲(0,0),並且重寫了draw(canvas)
和dispatchDraw(canvas)
方法,並且沒有做任何繪製操作。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//指定尺寸爲0,0
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
setVisibility()
和inflate()
方法
//定義了一個View的弱引用
private WeakReference<View> mInflatedViewRef;
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
//如果弱引用不爲空且View不爲空,調用View的setVisibility方法
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) {
//弱引用爲空且可見性設置爲VISIBLE或者INVISIBLE,調用inflate()方法
inflate();
}
}
}
到這裏基本可以分析出弱引用持有的對象就是替換佈局的View。繼續往下看mInflatedViewRef是在哪裏初始化的。
inflate()方法,核心方法執行具體的佈局替換操作。
public View inflate() {
//獲取父佈局
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
//獲取要替換的View對象
final View view = inflateViewNoAdd(parent);
//執行替換操作
replaceSelfWithView(view, parent);
//初始化弱引用持有View對象
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");
}
}
inflate()方法中獲取要替換的View對象並執行了替換操作,mInflatedViewRef持有的確實是替換View對象的實例。
ViewStub加載完指定佈局之後會被移除,不再佔用空間
我們繼續來看inflateViewNoAdd()
方法和replaceSelfWithView()
方法。
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
//動態加載View
final View view = factory.inflate(mLayoutResource, parent, false);
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
inflateViewNoAdd()
方法比較簡單,沒什麼好解釋的。
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);
}
}
replaceSelfWithView()
執行了移除和替換兩步操作。這也解釋了爲什麼inflate()
方法只能執行一次,因爲執行replaceSelfWithView()
自身已經被移除,再次執行inflate()
方法獲取getParent()
會爲空,從而拋出IllegalStateException
異常。
使用場景
app頁面中總會有一些佈局是不常顯示的,如一些特殊提示和頁面loading等,這時可以使用ViewStub來實現懶加載的功能,優化UI性能。
總結
ViewStub雖然實現簡單,但是源碼設計巧妙。對於頁面中的不常用佈局使用ViewSutb懶加載有一定的優化效果。
Merge的使用
簡介
- merge既不是View也不是ViewGroup,只是一種標記。
- merge必須在佈局的根節點。
- 當merge所在佈局被添加到容器中時,merge節點被合併不佔用佈局,merge下面的所有視圖轉移到容器中。
使用
通過一種比較常用的場景來比較下使用merge和不使用的區別。
不使用merge
Activity佈局:
<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" >
<RelativeLayout
android:id="@+id/title_rel"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/layout_merge"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/content_rel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/title_rel"/>
</RelativeLayout>
ToolBar佈局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools" >
<ImageView
android:id="@+id/home_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/home_iv"
android:gravity="center_vertical"
android:textSize="25sp"
android:textColor="#000000"
tools:text="測試標題"/>
</RelativeLayout>
實際Activity佈局層級:
<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" >
<RelativeLayout
android:id="@+id/title_rel"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<ImageView
android:id="@+id/home_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/home_iv"
android:gravity="center_vertical"
android:textSize="25sp"
android:textColor="#000000"
tools:text="測試標題"/>
</RelativeLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/content_rel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/title_rel"/>
</RelativeLayout>
使用merge進行優化:
優化後的ToolBar佈局:
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.RelativeLayout">
<ImageView
android:id="@+id/home_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/home_iv"
android:gravity="center_vertical"
android:textSize="25sp"
android:textColor="#000000"
tools:text="測試標題"/>
</merge>
使用tools:parentTag
屬性可以指定父佈局類型,方便在Android Studio中編寫佈局時進行預覽。
實際Activity佈局層級:
<?xml version="1.0" encoding="utf-8"?>
<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" >
<RelativeLayout
android:id="@+id/title_rel"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/home_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/home_iv"
android:gravity="center_vertical"
android:textSize="25sp"
android:textColor="#000000"
tools:text="測試標題"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/content_rel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/title_rel"/>
</RelativeLayout>
可以看到使用merge之後佈局層級減少了一層。
使用場景
上面例子可能不太合適,這麼寫佈局容易被打。
來看一種使用頻率更高的應用場景——自定義View,大家應該都實現過,比如要定義一個通用的天氣控件,通常是自定義一個WeatherView 繼承自RelativeLayout,然後通過inflate動態引入佈局,那麼佈局怎麼寫呢?不使用merge的情況下根佈局肯定是RelativeLayout,引入WeatherView之後豈不是嵌套了一層RelativeLayout。這時候就可以在佈局中使用merge進行優化。
還有一種應用場景,如果Activity的根佈局是FrameLayout可以使用merge進行替換,使用之後可以使Activity的佈局層級減少一層。爲什麼會這樣呢?首先我們要了解Activity頁面的佈局層級,最外層是PhoneWindow其下是一個DecorView下面就是TitleView和ContentView,ContentView就是我們通過SetContentView設置的Activity的佈局,沒錯ContentView是一個FrameLayout,所以在Activity佈局中使用merge可以減少層級。
總結
正確的使用merge可以有效的減少佈局層級,提高頁面渲染速度。但是merge使用限制比較多,應用場景比較少。