佈局優化ViewStub

尊重原創,轉載請註明出處: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

總結

  1. ViewStub標籤需要必須通過android:layout屬性指定待加載的視圖資源文件ID,否則會拋異常。
  2. ViewStub標籤的所有android:layout_打頭的屬性,都會替換待加載視圖的跟佈局對應屬性
  3. 最好通過ViewStub的inflate方法加載視圖,該方法會返回視圖根節點。
  4. inflate方法只能調用一次,不建議通過setVisibility加載視圖
  5. 如果需要通過findViewById查找待加載視圖中的節點,需要在inflate方法執行之後,否則會找不到
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章