ViewStub
簡介
ViewStub是一種沒有任何維度的輕量型視圖,它不會(huì)繪制任何內(nèi)容或參與布局。
- ViewStub是一種沒有大小,不占用布局的View。
- 直到當(dāng)調(diào)用
inflate()方法或者可見性變?yōu)閂ISIBLE時(shí),才會(huì)將指定的布局加載到父布局中。 - ViewStub加載完指定布局之后會(huì)被移除,不再占用空間。(所以
inflate()方法只能調(diào)用一次 )
因?yàn)檫@些特性ViewStub可以用來懶加載布局,優(yōu)化UI性能。
使用
布局
在布局中添加ViewStub標(biāo)簽并通過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" />
代碼
在需要展示布局的地方調(diào)用 inflate() 方法或者將ViewStub的可見性設(shè)置為VISIBLE。
private View viewStubContentView = null;
visibleViewStub.setVisibility(View.VISIBLE);
if(viewStubContentView == null){
viewStubContentView = inflateViewStub.inflate();
}
注意 :inflate() 方法只能調(diào)用一次,重復(fù)調(diào)用被拋出IllegalStateException異常。
inflate() 方法會(huì)返回替換的布局的根View而設(shè)置VISIBLE不會(huì)返回,如果需要獲取替換布局的實(shí)例,如:需要為替換的布局設(shè)置監(jiān)聽事件,這是需要使用inflate() 方法而不是VISIBLE。
ViewStub源碼分析
針對(duì)我們前面說的ViewStub的幾個(gè)特點(diǎn),我們來分析下源碼是如何實(shí)現(xiàn)的。分析源碼可以學(xué)習(xí)別人優(yōu)秀的代碼設(shè)計(jì),也可以為我們?nèi)蘸箢愃菩枨蟮膶?shí)現(xiàn)提供借鑒。
ViewSutb沒有大小,不占用布局
ViewStub在構(gòu)造方法中設(shè)置了控件可見性為GONE并且指定不進(jìn)行繪制。
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();
//設(shè)置不可見
setVisibility(GONE);
//指定不進(jìn)行繪制
setWillNotDraw(true);
}
并且重寫了onMeasure(widthMeasureSpec, heightMeasureSpec)設(shè)置尺寸為(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()方法
//定義了一個(gè)View的弱引用
private WeakReference<View> mInflatedViewRef;
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
//如果弱引用不為空且View不為空,調(diào)用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) {
//弱引用為空且可見性設(shè)置為VISIBLE或者INVISIBLE,調(diào)用inflate()方法
inflate();
}
}
}
到這里基本可以分析出弱引用持有的對(duì)象就是替換布局的View。繼續(xù)往下看mInflatedViewRef是在哪里初始化的。
inflate()方法,核心方法執(zhí)行具體的布局替換操作。
public View inflate() {
//獲取父布局
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
//獲取要替換的View對(duì)象
final View view = inflateViewNoAdd(parent);
//執(zhí)行替換操作
replaceSelfWithView(view, parent);
//初始化弱引用持有View對(duì)象
mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
//觸發(fā)監(jiān)聽
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對(duì)象并執(zhí)行了替換操作,mInflatedViewRef持有的確實(shí)是替換View對(duì)象的實(shí)例。
ViewStub加載完指定布局之后會(huì)被移除,不再占用空間
我們繼續(xù)來看inflateViewNoAdd() 方法和replaceSelfWithView()方法。
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
//動(dòng)態(tài)加載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()執(zhí)行了移除和替換兩步操作。這也解釋了為什么inflate()方法只能執(zhí)行一次,因?yàn)閳?zhí)行replaceSelfWithView()自身已經(jīng)被移除,再次執(zhí)行inflate()方法獲取getParent()會(huì)為空,從而拋出IllegalStateException異常。
使用場景
app頁面中總會(huì)有一些布局是不常顯示的,如一些特殊提示和頁面loading等,這時(shí)可以使用ViewStub來實(shí)現(xiàn)懶加載的功能,優(yōu)化UI性能。
總結(jié)
ViewStub雖然實(shí)現(xiàn)簡單,但是源碼設(shè)計(jì)巧妙。對(duì)于頁面中的不常用布局使用ViewSutb懶加載有一定的優(yōu)化效果。
Merge
簡介
- merge既不是View也不是ViewGroup,只是一種標(biāo)記。
- merge必須在布局的根節(jié)點(diǎn)。
- 當(dāng)merge所在布局被添加到容器中時(shí),merge節(jié)點(diǎn)被合并不占用布局,merge下面的所有視圖轉(zhuǎn)移到容器中。
使用
通過一種比較常用的場景來比較下使用merge和不使用的區(qū)別。
不使用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="測試標(biāo)題"/>
</RelativeLayout>
實(shí)際Activity布局層級(jí):
<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="測試標(biāo)題"/>
</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進(jìn)行優(yōu)化:
優(yōu)化后的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="測試標(biāo)題"/>
</merge>
使用tools:parentTag屬性可以指定父布局類型,方便在Android Studio中編寫布局時(shí)進(jìn)行預(yù)覽。
實(shí)際Activity布局層級(jí):
<?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="測試標(biāo)題"/>
</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之后布局層級(jí)減少了一層。
使用場景
上面例子可能不太合適,這么寫布局容易被打。
來看一種使用頻率更高的應(yīng)用場景——自定義View,大家應(yīng)該都實(shí)現(xiàn)過,比如要定義一個(gè)通用的天氣控件,通常是自定義一個(gè)WeatherView 繼承自RelativeLayout,然后通過inflate動(dòng)態(tài)引入布局,那么布局怎么寫呢?不使用merge的情況下根布局肯定是RelativeLayout,引入WeatherView之后豈不是嵌套了一層RelativeLayout。這時(shí)候就可以在布局中使用merge進(jìn)行優(yōu)化。
還有一種應(yīng)用場景,如果Activity的根布局是FrameLayout可以使用merge進(jìn)行替換,使用之后可以使Activity的布局層級(jí)減少一層。為什么會(huì)這樣呢?首先我們要了解Activity頁面的布局層級(jí),最外層是PhoneWindow其下是一個(gè)DecorView下面就是TitleView和ContentView,ContentView就是我們通過SetContentView設(shè)置的Activity的布局,沒錯(cuò)ContentView是一個(gè)FrameLayout,所以在Activity布局中使用merge可以減少層級(jí)。
總結(jié)
正確的使用merge可以有效的減少布局層級(jí),提高頁面渲染速度。但是merge使用限制比較多,應(yīng)用場景比較少。