從源碼角度分析ViewStub 疑問與原理,源碼viewstub
一、提出疑問
ViewStub比較簡單,之前文章都提及到《Android 效能最佳化 三 布局最佳化ViewStub標籤的使用》,但是在使用過程中有一個疑惑,到底是ViewStub上設定的參數有效還是在其包括的layout中設定參數有效?如果不明白描述的問題,可以看下以下布局虛擬碼。
res/layout/main.xml<LinearLayout > <ViewStub android:id="@+id/viewstub" android:layout_width="100dip" android:layout_marginTop="100dip" android:layout_height="wrap_content" android:layout="@layout/sub_layout" /> </LinearLayout>res/layout/sub_layout.xml<TextView android:layout_width="50dip" android:layout_marginTop="50dip" android:layout_height="wrap_content" android:text="ViewStub中包含的TextVeiw"/>
上面的代碼中width最終效果是100dip還是50dip?marginTop是100dip還是50dip?帶著這個問題一起看下Android 5.0源碼看看ViewStub原理。
為了便於把ViewStub與其infalte()載入出來的android:layout視圖做個區分,下文中針對前者統一命名“ViewStub視圖”,後者命名“被 載入視圖”,僅為了描述統一併不一定是專業名稱。
二、分析ViewStub源碼
讓ViewStub有兩種方式一種是調用ViewStub.inflate() 另外一種是設定ViewStub.setVisibility(View.VISIBLE); 其實第二種方式依然是調用的infalte方法,可以看如下ViewStub源碼。
@Override @android.view.RemotableViewMethod 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(); } } }
ViewStub複寫了setVisibility方法,並在其中調用infalte方法,下面來看此方法源碼
public final class ViewStub extends View { ...... public View inflate() { final ViewParent viewParent = getParent(); // 1 為什麼可以直接擷取父視圖? // ViewStub的父視圖必須是ViewGroup的子類 if (viewParent != null && viewParent instanceof ViewGroup) { if (mLayoutResource != 0) { // ViewStub必須設定android:layout屬性 final ViewGroup parent = (ViewGroup) viewParent; final LayoutInflater factory; if (mInflater != null) { factory = mInflater; } else { factory = LayoutInflater.from(mContext); } // 2 inflate被載入視圖 final View view = factory.inflate(mLayoutResource, parent, false); if (mInflatedId != NO_ID) { view.setId(mInflatedId); } // 從父視圖中擷取當前ViewStub在父視圖中的位置 final int index = parent.indexOfChild(this); // 當前ViewStub也是個View僅僅只是用來佔位,所以先把佔位的ViewStub視圖刪除 parent.removeViewInLayout(this); // 3 此處擷取的是ViewStub上面設定的參數 final ViewGroup.LayoutParams layoutParams = getLayoutParams(); if (layoutParams != null) { parent.addView(view, index, layoutParams); } else { parent.addView(view, index); } // 目的是在複寫的setVisibility方法中使用 // 因為ViewStub.setVisibility操作的是被載入視圖並非當前ViewStub視圖 mInflatedViewRef = new WeakReference<View>(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"); } } ......}
下面說下源碼中列出的幾點。
1. 為什麼可以直接擷取父視圖?
ViewStub 繼承自View其自身就是一個視圖,其調用getParent()可以從父類View上入手、
public class View { public final ViewParent getParent() { return mParent; } void assignParent(ViewParent parent) { if (mParent == null) { mParent = parent; } else if (parent == null) { mParent = null; } else { throw new RuntimeException("view " + this + " being added, but" + " it already has a parent"); } }}
從View的源碼中擷取到,修改mParent參數的僅有assignParent方法且View中並未調用此方法,下面查看下其子類ViewGroup是否有調用。
public class ViewGroup { public void addView(View child, int index, LayoutParams params) { ...... addViewInner(child, index, params, false); } private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { ...... // tell our children if (preventRequestLayout) { child.assignParent(this); } else { child.mParent = this; } ...... } }
從上面源碼可以看到在addView方法中會調用addViewInner,其中調用child.assignParent(this);,把自己所有子視圖mParent都設定成當前ViewGroup。從這一點也可以看出,ViewStub本身是一個View且載入的時候就已經添加到視圖樹中(View Tree)中,僅接著有另外一個問題既然頁面顯示的時候ViewStub已經被添加到介面上,為什麼有看不到ViewStub視圖呢?
疑問:為什麼ViewStub雖然是懶載入,但是其自身是一個視圖且展示介面就會添加到視圖樹中,為什麼看不到ViewStub?
public final class ViewStub extends View { public ViewStub(Context context) { initialize(context); } private void initialize(Context context) { mContext = context; setVisibility(GONE); // 初始化時把自己設定為隱藏 setWillNotDraw(true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(0, 0); // 所有子視圖都設定為寬高為0 } @Override public void draw(Canvas canvas) { // 不對自身與子視圖進行繪製 } @Override protected void dispatchDraw(Canvas canvas) { }}
從以上源碼可以看出ViewStub用盡所有辦法讓自己添加到視圖樹上是不顯示ViewStub自身。
2. inflate被載入視圖 再來看下載入android:layout視圖的源碼。final View view = factory.inflate(mLayoutResource, parent, false); 可以看到通過infalte方法記載的,其三個參數(int resource, ViewGroup root, boolean attachToRoot),分別是:mLayoutResource : 設定的android:layout的值 parent : 通過getParent()擷取即ViewStub的父視圖 false : attachToRoot設定為false說明忽略androd:layout中根節點的layoutParams參數,即width=50dip與margin50dip
3. 視圖添加ViewStub.getLayoutParams參數此處源碼的是擷取ViewStub.getLayoutParams參數設定到anroid:layout載入的視圖上, 即width=100dip與marginTop=100dip生效。
三、總結開頭的疑問的答案,inflate出來的視圖width=100dip與marginTop=100dip而android:layout視圖中設定的width50dip和marginTop=50dip失效,等於沒有設定。
ViewStub的原理簡單描述是1. ViewStub本身是一個視圖,會被添加到介面上,之所以看不到是因為其源碼設定為隱藏與不繪製。2. 當調用infalte或者ViewStub.setVisibility(View.VISIBLE);時(兩個都使用infalte方法邏輯),先從父視圖上把當前ViewStub刪除,再把載入的android:layotu視圖添加上3. 把ViewStub layoutParams 添加到載入的android:layotu視圖上,而其根節點layoutParams 設定無效。4. ViewStub是指用來佔位的視圖,通過刪除自己並添加android:layout視圖達到懶載入效果