尊重原創,轉載請註明出處:http://blog.csdn.net/a740169405/article/details/50351013
前言:
在設計模式的單利模式中,懶漢式和餓漢式是其中兩種。
一種是在類被加載的時候就完成單例對象的初始化,一種是在需要使用該單例的時候才初始化。
在android的視圖設計中,同樣需要使用的這樣的設計模式。
這樣的視圖加載起來需要耗費很多的時間。在這幾百個視圖裏面,可能有部分視圖是在點擊某一按鈕也就是並不是馬上加載,
而是延遲到要使用的時候才加載這部分視圖。也就是類似於單例模式中的懶加載。
特性:
1. ViewStub是一個繼承了View類的視圖。
2. ViewStub是不可見的,實際上是把寬高都設置爲0
3. 可以通過佈局文件的android:inflatedId或者調用ViewStub的setInflatedId方法爲懶加載視圖的跟節點設置ID
4. ViewStub視圖在首次調用setVisibility或者inflate方法之前,一直存在於視圖樹中
5. 只需要調用ViewStub的setVisibility或者inflate方法即可顯示懶加載的視圖
6. 調用setVisibility或者inflate方法之後,懶加載的視圖會把ViewStub從父節點中替換掉
7. ViewStub的inflate只能被調用一次,第二次調用會拋出異常,setVisibility可以被調用多次,但不建議這麼做(後面說原因)
8. 爲ViewStub賦值的android:layout_屬性會替換待加載佈局文件的根節點對應的屬性
9. inflate方法會返回待加載視圖的根節點
使用:
我在一個activity上放置了一個按鈕,點擊後加載懶加載的視圖。
Activity佈局文件定義my_sub_activity.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:onClick="onClick"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="加載視圖"/>
<ViewStub
android:id="@+id/stub"
android:inflatedId="@+id/subTree"
android:layout="@layout/my_sub_tree"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
其中Android:inflatedId指定了懶加載視圖跟節點的ID。android:layout指定了懶加載的視圖。android:layout_width和android:layout_height分別指定了懶加載視圖的寬和高。
懶加載佈局文件my_sub_tree.xml:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center"
android:padding="10dip"
android:text="懶加載視圖"
android:textColor="#000000"
android:textSize="22sp">
</TextView>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
懶加載視圖裏只有一個TextView(這裏只是做測試,正常情況下這裏應該是一個複雜的視圖)。
ViewStubActivity的代碼:
public class ViewStubActivity extends Activity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_sub_activity);
}
@Override
public void onClick(View v) {
// 這裏調用的是inflate方法,當然,也可以調用setVisibility方法(但是不建議這麼做)
// 只能點擊一次加載視圖按鈕,因爲inflate只能被調用一次
// 如果再次點擊按鈕,會拋出異常"ViewStub must have a non-null ViewGroup viewParent"
((ViewStub) findViewById(R.id.stub)).inflate();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
代碼裏設置了佈局,並在點擊後查到到ViewStub對象,並加載視圖。
下面看看加載視圖前後的對比圖:
爲了說明視圖樹在加載前後的對比,我使用hierarchyviewer視圖樹查看工具,做了一個前後對比圖:
加載前視圖樹:
加載後視圖樹:
從上面的兩個視圖樹中我們明顯發現,ViewStub節點被TextView替換。
也就是說,在調用inflate方法之前,ViewStub一直存在於視圖樹中,當調用inflate之後,ViewStub被加載的視圖替換,到此,ViewStub的作用完成,之後ViewStub可能被內存回收(如果沒有聲明成成員變量的話,也就是沒有強引用)
源碼解析:
下面針對ViewStub的特性對源碼進行解析:
特性一:ViewStub是一個繼承了View類的視圖。
public final class ViewStub extends View {
特性二:ViewStub是不可見的,實際上是把寬高都設置爲0
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 測量的時候,告訴父節點自己需要的空間爲0
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
// 不繪製
}
@Override
protected void dispatchDraw(Canvas canvas) {
// 不分發繪製事件
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
ViewStub在計算的時候,爲自己請求的寬高都爲0,並重寫了繪製相關的方法,但不做任何事情。
特性三:可以通過佈局文件的android:inflatedId或者調用ViewStub的setInflatedId方法爲懶加載視圖的跟節點設置ID(如果跟視圖未設置ID的話)
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ViewStub, defStyleAttr, defStyleRes);
// 通過自屬性inflatedId來獲取加載的視圖跟節點ID,默認返回NO_ID,也就是-1,代表沒有賦值id
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
// 需要加載的視圖資源ID
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
a.recycle();
a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
// 爲自己賦值ID,沒有則賦值爲-1
mID = a.getResourceId(R.styleable.View_id, NO_ID);
a.recycle();
// 初始化視圖
initialize(context);
}
private void initialize(Context context) {
mContext = context;
// 設置視圖不可見
setVisibility(GONE);
// 設置當前視圖不可繪製
setWillNotDraw(true);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
初始化的時候,從配置文件中取出了inflatedId和待加載的資源文件id以及自身的id,
最後,調用了initialize將自身設置爲不可見,並設置爲不可重繪,最大限度減少資源佔用。
最後看看什麼時候ViewStub執行加載視圖操作:
首先是inflate方法:
/**
* 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) {
// 判斷父節點不爲空,並且是容器,則進行視圖加載
if (mLayoutResource != 0) {
// 必須在佈局文件中,或者是調用setLayoutResource方法設置待加載的視圖資源文件ID
final ViewGroup parent = (ViewGroup) viewParent;
final LayoutInflater factory;
// mInflater是外部設置進來的,通過setLayoutInflater方法設置
if (mInflater != null) {
factory = mInflater;
} else {
// 如果外部未設置視圖加載器,初始化
factory = LayoutInflater.from(mContext);
}
// 加載視圖,得到視圖根節點
final View view = factory.inflate(mLayoutResource, parent,
false);
if (mInflatedId != NO_ID) {
// 如果有設置inflateId,則賦值給根節點(如果根節點自己有id,會被覆蓋)
view.setId(mInflatedId);
}
// 得到ViewStub在父節點中的位置
final int index = parent.indexOfChild(this);
// 從父節點中移除ViewStub(到此,ViewStub從視圖樹種移除)
parent.removeViewInLayout(this);
// 得到ViewStub在佈局文件中定義的android:layout_*的屬性
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 將懶加載視圖添加到ViewStub的父節點(到此,ViewStub被完全替換)
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
// 將懶加載的視圖使用弱引用進行引用(給setVisibility方法使用,後面會講)
mInflatedViewRef = new WeakReference<View>(view);
// 視圖加載成功後調用回調方法。
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
// 返回加載後佈局文件的根節點
return view;
} else {
// 如果未設置layoutResource也就是待加載的視圖,則拋出異常
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
// 如果ViewStub的父節點爲空,因爲ViewStub成功執行inflate方法後
// 會調用parent.removeViewInLayout(this);將自己從父節點移除
// 所以ViewStub的inflate只能調用一次
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
ViewStub的inflate方法簡要的講就是把自己從父親從移除,把待加載的視圖加入到父節點中,
並把自己所有的layout屬性給待加載的視圖,
什麼是layout屬性呢,也就是下面以”android:layout_”打頭的屬性:
如android:layout_width以及layout_height,
所以這裏大家需要小心自己的待加載視圖的根節點的android:layout_屬性被替換掉。
<ViewStub
android:id="@+id/stub"
android:inflatedId="@+id/subTree"
android:layout="@layout/my_sub_tree"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
接着我們有提到,調用ViewStub的setVisibility也可以加載待加載視圖:
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
// 如果對待加載視圖的軟引用不爲空,說明已經執行過inflate方法了
// 因爲在inflate方法執行成功後有對其賦值
View view = mInflatedViewRef.get();
if (view != null) {
// 如果引用的視圖未被垃圾回收器回收,則設置其可見性
view.setVisibility(visibility);
} else {
// 如果引用的視圖已經被垃圾回收器回收,則拋出異常
// 這也就是爲什麼setVisibility可以調用多次,但是並不推薦這樣做的原因
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
// 如果弱引用對象未初始化,則說明未調用inflate
// 設置自身可見性
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
// 如果傳入的是VISIBLE或者INVIBLE,則調用inflate加載視圖
inflate();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
另外ViewStub還提供了一系列方法,供用戶設置屬性:
/** 獲取待加載視圖的根節點ID */
public int getInflatedId() {
return mInflatedId;
}
/** 設置待加載視圖的根節點ID */
public void setInflatedId(int inflatedId) {
mInflatedId = inflatedId;
}
/** 獲取待加載視圖的資源文件ID */
public int getLayoutResource() {
return mLayoutResource;
}
/** 設置待加載視圖的資源文件ID */
public void setLayoutResource(int layoutResource) {
mLayoutResource = layoutResource;
}
/** 設置佈局加載器 */
public void setLayoutInflater(LayoutInflater inflater) {
mInflater = inflater;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
總結
- ViewStub標籤需要必須通過android:layout屬性指定待加載的視圖資源文件ID,否則會拋異常。
- ViewStub標籤的所有android:layout_打頭的屬性,都會替換待加載視圖的跟佈局對應屬性
- 最好通過ViewStub的inflate方法加載視圖,該方法會返回視圖根節點。
- inflate方法只能調用一次,不建議通過setVisibility加載視圖
- 如果需要通過findViewById查找待加載視圖中的節點,需要在inflate方法執行之後,否則會找不到