目錄介紹
- 01.什麼是ViewStub
- 02.ViewStub構造方法
- 03.inflate()方法解析
- 04.WeakReference使用
- 05.ViewStub為何無大小
- 06.ViewStub為何不繪製
- 07.可以多次inflate()嗎
- 08.ViewStub不支援merge
- 09.ViewStub使用場景
- 10.ViewStub總結分析
好訊息
- 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
- 連結地址:github.com/yangchong21…
- 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!
01.什麼是ViewStub
- ViewStub 是一個看不見的,沒有大小,不佔佈局位置的 View,可以用來懶載入佈局。
- 當 ViewStub 變得可見或
inflate()
的時候,佈局就會被載入(替換 ViewStub)。因此,ViewStub 一直存在於檢視層次結構中直到呼叫了setVisibility(int)
或inflate()
。 - 在 ViewStub 載入完成後就會被移除,它所佔用的空間就會被新的佈局替換。
02.ViewStub構造方法
- 先來看看構造方法:
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewStub, defStyleAttr, defStyleRes); // 要被載入的佈局 Id mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID); // 要被載入的佈局 mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0); // ViewStub 的 Id mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID); a.recycle(); // 初始狀態為 GONE setVisibility(GONE); // 設定為不會繪製 setWillNotDraw(true); } 複製程式碼
- 接下來就看看關鍵的方法,然後看看初始化狀態setVisibility方法。
// 複寫了 setVisibility(int) 方法 @Override @android.view.RemotableViewMethod public void setVisibility(int visibility) { // private WeakReference<View> mInflatedViewRef; // mInflatedViewRef 是對佈局的弱引用 if (mInflatedViewRef != null) { // 如果不為 null,就拿到懶載入的 View View view = mInflatedViewRef.get(); if (view != null) { // 然後就直接對 View 進行 setVisibility 操作 view.setVisibility(visibility); } else { // 如果為 null,就丟擲異常 throw new IllegalStateException("setVisibility called on un-referenced view"); } } else { super.setVisibility(visibility); // 之前說過,setVisibility(int) 也可以進行載入佈局 if (visibility == VISIBLE || visibility == INVISIBLE) { // 因為在這裡呼叫了 inflate() inflate(); } } } 複製程式碼
03.inflate()方法解析
- 核心來了,平時用的時候,會經常呼叫到該方法。inflate() 是關鍵的載入實現,程式碼如下所示:
public View inflate() { // 獲取父檢視 final ViewParent viewParent = getParent(); if (viewParent != null && viewParent instanceof ViewGroup) { // 如果沒有指定佈局,就會丟擲異常 if (mLayoutResource != 0) { // viewParent 需為 ViewGroup final ViewGroup parent = (ViewGroup) viewParent; final LayoutInflater factory; if (mInflater != null) { factory = mInflater; } else { // 如果沒有指定 LayoutInflater factory = LayoutInflater.from(mContext); } // 獲取佈局 final View view = factory.inflate(mLayoutResource, parent, false); // 為 view 設定 Id if (mInflatedId != NO_ID) { view.setId(mInflatedId); } // 計算出 ViewStub 在 parent 中的位置 final int index = parent.indexOfChild(this); // 把 ViewStub 從 parent 中移除 parent.removeViewInLayout(this); // 接下來就是把 view 加到 parent 的 index 位置中 final ViewGroup.LayoutParams layoutParams = getLayoutParams(); if (layoutParams != null) { // 如果 ViewStub 的 layoutParams 不為空 // 就設定給 view parent.addView(view, index, layoutParams); } else { parent.addView(view, index); } // mInflatedViewRef 就是在這裡對 view 進行了弱引用 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"); } } 複製程式碼
- Inflate使用特點
- ViewStub只能被Inflate一次,inflate之後ViewStub物件就會被置為空。即某個被ViewStub指定的佈局被Inflate後,就不能夠再通過ViewStub來控制它了。
- ViewStub只能用來Inflate一個佈局檔案,而不是某個具體的View,當然也可以把View寫在某個佈局檔案中。
04.WeakReference使用
- 使用了弱引用管理物件的建立,程式碼如下所示
- 在這裡使用了get方法
@Override @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync") 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 { } } 複製程式碼
- 在這裡建立了弱引用物件
public View inflate() { final ViewParent viewParent = getParent(); if (viewParent != null && viewParent instanceof ViewGroup) { if (mLayoutResource != 0) { mInflatedViewRef = new WeakReference<>(view); return view; } else { throw new IllegalArgumentException("ViewStub must have a valid layoutResource"); } } } 複製程式碼
05.ViewStub為何無大小
- 首先先看一段原始碼,如下所示:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(0, 0); } @Override public void draw(Canvas canvas) { } @Override protected void dispatchDraw(Canvas canvas) { } 複製程式碼
- 有沒有覺得很與眾不同
- draw和dispatchDraw雖然重寫了,但是看程式碼卻都是什麼也不做!並且onMeasure還什麼也不做,直接setMeasuredDimension(0,0);來把view區域設定位0,原來一個ViewStub雖然是一個view,卻是一個沒有任何顯示內容,也不顯示任何內容的特殊view,並且對layout在載入時候不可見的。
06.ViewStub為何不繪製
- 具體看一下setWillNotDraw(true)方法,程式碼如下:
public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); } 複製程式碼
- View中,對於WILL_NOT_DRAW是這樣定義的:
/** * This view won't draw. {@link #onDraw(android.graphics.Canvas)} won't be * called and further optimizations will be performed. It is okay to have * this flag set and a background. Use with DRAW_MASK when calling setFlags. * {@hide} */ static final int WILL_NOT_DRAW = 0x00000080; 複製程式碼
- 設定WILL_NOT_DRAW之後,onDraw()不會被呼叫,通過略過繪製的過程,優化了效能。在ViewGroup中,初始化時設定了WILL_NOT_DRAW,程式碼如下:
public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initViewGroup(); initFromAttributes(context, attrs, defStyleAttr, defStyleRes); } private void initViewGroup() { // ViewGroup doesn't draw by default if (!debugDraw()) { setFlags(WILL_NOT_DRAW, DRAW_MASK); } mGroupFlags |= FLAG_CLIP_CHILDREN; mGroupFlags |= FLAG_CLIP_TO_PADDING; mGroupFlags |= FLAG_ANIMATION_DONE; mGroupFlags |= FLAG_ANIMATION_CACHE; mGroupFlags |= FLAG_ALWAYS_DRAWN_WITH_CACHE; if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) { mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS; } setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS); mChildren = new View[ARRAY_INITIAL_CAPACITY]; mChildrenCount = 0; mPersistentDrawingCache = PERSISTENT_SCROLLING_CACHE; } 複製程式碼
- 所以,在寫自定義佈局時,如果需要呼叫onDraw()進行繪製,則需要在初始化時候,呼叫setWillNotDraw(false)。若是想要更進一步閱讀View中WILL_NOT_DRAW的相關原始碼,可以去看下PFLAG_SKIP_DRAW相關的程式碼。
07.可以多次inflate()嗎
- ViewStub物件只可以Inflate一次,之後ViewStub物件會被置為空。同時需要注意的問題是,inflate一個ViewStub物件之後,就不能再inflate它了,否則會報錯:ViewStub must have a non-null ViewGroup viewParent。。
- 其實看一下原始碼就很好理解:
public View inflate() { //獲取viewStub的父容器物件 final ViewParent viewParent = getParent(); if (viewParent != null && viewParent instanceof ViewGroup) { if (mLayoutResource != 0) { final ViewGroup parent = (ViewGroup) viewParent; //這裡是載入佈局,並且給它設定id //佈局的載入是通過LayoutInflater解析出來的 final View view = inflateViewNoAdd(parent); //這行程式碼很重要,下面會將到 replaceSelfWithView(view, parent); //使用弱引用 mInflatedViewRef = new WeakReference<>(view); if (mInflateListener != null) { mInflateListener.onInflate(this, view); } return view; } else { //如果已經載入出來,再次inflate就會丟擲異常呢 throw new IllegalArgumentException("ViewStub must have a valid layoutResource"); } } else { throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent"); } } 複製程式碼
- 其實也可以用一張圖來理解它,如下所示,摘自網路
- 也就是說,一旦呼叫inflate上面的方法後ViewStub就會變成null了,因此使用該物件特別需要注意空指標問題。
08.ViewStub不支援merge
- 不能引入包含merge標籤的佈局到ViewStub中。否則會報錯:android.view.InflateException: Binary XML file line #1: can be used only with a valid ViewGroup root and attachToRoot=true
09.ViewStub使用場景
- 一般的app中大多有這麼一個功能,當載入的資料為空時顯示一個資料為空的檢視、在資料載入失敗時顯示載入失敗對應的UI,當沒有網路的時候載入沒有網路的UI,並支援點選重試會比白屏的使用者體驗更好一些。俗稱,頁面狀態切換管理……一般來說,載入中、載入失敗、空資料等狀態的UI風格,在App內的所有頁面中需要保持一致,也就是需要做到全域性統一,也支援區域性定製。
- ViewStub的優勢在於在上面的場景中,並不一定需要把所有的內容都展示出來,可以隱藏一些View檢視,待使用者需要展示的時候再載入到當前的Layout中,這個時候就可以用到ViewStub這個控制元件了,這樣可以減少資源的消耗,使最初的載入速度變快。
- 那麼就有了之前開發使用的狀態管理器開源庫,就是採用了ViewStub這個控制元件,讓View狀態的切換和Activity徹底分離開。用builder模式來自由的新增需要的狀態View,可以設定有資料,資料為空,載入資料錯誤,網路錯誤,載入中等多種狀態,並且支援自定義狀態的佈局。可以說完全不影響效能……
10.ViewStub總結分析
- 分析原始碼的原理,不管認識到哪一步,最終的目標還是在運用上,即把看原始碼獲得的知識用到實際開發中,那麼關於ViewStub的使用技巧,具體可以看我的狀態管理器案例,連結地址:github.com/yangchong21…
- 歡迎你的star,這也是開源和寫部落格的源源動力,哈哈