android: View的生命週期
今天看到了一篇不錯的文章,是一位外國小哥寫的,個人覺得不錯,遂翻譯之,英文好的同學可以直接移步 ——> 生肉:https://proandroiddev.com/the-life-cycle-of-a-view-in-android-6a2c4665b95e
概述
當我們檢視一款App的時候,首先引起我們注意的就是螢幕上顯示的內容,而螢幕上顯示的內容就是View。
View是UI介面的基本構建塊,它佔據了一塊矩形區域,負責繪圖和事件處理。View同時也是android上其它UI控制元件的基類,可以用來建立其它互動式的UI元件(比如Button, TextView,等等)。View的子類ViewGroup
下圖描述了android上的View與其它UI元件之間的關係:
View的生命週期
每個Activity都有自己的生命週期,同樣,View也有自己的生命週期。在螢幕上渲染的View必須經歷這些生命週期方法才能正確地在螢幕上繪製。這些生命週期方法的每一種都有其重要性。下面我們來深入瞭解下View的生命週期:
建構函式
通常,我們對於View為什麼有四種類型的建構函式感到困惑:
View(Context context) View(Context context, @Nullable AttributeSet attrs) View(Context context, @Nullable AttributeSet attrs,int defStyleAttr) View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
View(Context context)
當從程式碼動態的建立View時就會用到這個簡易的構造方法。這裡的引數context是執行檢視的上下文,通過它可以訪問到當前的主題(theme),資源(resources)等東西。
View(Context context, @Nullable AttributeSet attrs)
從XML佈局里加載View時呼叫的構造方法。當從XML檔案裡建立View同時也為這個View指定了一些相應的屬性時,就會呼叫此方法。這個版本的建構函式使用的預設樣式是0,因此唯一應用的屬性值是上下文主題和給定AttrubiteSet中的屬性值。
View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
從XML檔案執行載入並從主題屬性(defStyleAttr)中應用基本樣式。View的這個構造方法允許View在載入時使用它自己定義的基本樣式。比如,系統Button類的建構函式就呼叫了這個建構函式,並且為defStyleAttr 這個引數提供R.attr.buttonStyle的樣式屬性;這樣可以使得系統提供的按鈕主題樣式可以修改所有的View(特別是View的背景),以及Button類的樣式屬性。
引數defStyleAttr是當前主題的一個屬性,其中包含了對樣式資源的引用,這個樣式資源為View的屬性提供了一個預設值,不查詢預設值可以將其設定為0。
View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
從XML檔案執行載入,並從主題屬性或樣式資源中應用特定於類的基本樣式。View的這個構造方法允許View在載入時使用它自己的基本樣式,與上個構造方法類似。
引數defStyleRes是樣式資源的資源識別符號,為View提供預設值,僅當defStyleAttr為0或在主題中找不到時使用。如果不查詢預設值,則可以為0。
View的生命週期主要由三部分組成:
Attachment / Detachment (關聯/分離)
Traversals(遍歷)
State Save / Restore (狀態儲存/恢復)
一. Attachment / Detachment
這是將View關聯到視窗(Window)或從視窗分離時的階段。在此階段,我們有一些方法可以接收回調來執行適當的操作。
● onAttachedToWindow()
當View關聯到視窗時呼叫。在這個階段,View知道它處於活動的狀態並且具有可繪製的表面。因此,我們在此階段就可以開始分配任何資源或設定listeners。
●onDetachedFromWindow()
當View與視窗分離時將呼叫此方法。此時,View不再具有可繪製的表面。在此階段,你需要停止任何已執行的任務或清理任何已分配的資源。當我們在ViewGroup類上呼叫removeView()或者Activity被銷燬時將呼叫此方法。
●onFinishInflate()
當佈局載入器(LayoutInflater)從XML檔案裡完成了所有子View的載入時,將會呼叫此方法。
二. Traversals
之所以把此階段稱為“遍歷”,是因為View的檢視層次就像是從父節點(ViewGroup)到子節點的樹狀結構。因此,每個方法都是從父節點開始,一直遍歷執行到最後一個節點:
測量(Measure)階段和佈局(Layout)階段始終是一起發生。如上圖所示,這是一個順序的過程。
●onMeasure()
這一步用於測量View到底應該有多大。如果是ViewGroup,它會對它的每一個子View都呼叫測量,測量的結果可以幫助ViewGroup確定其自身的大小。
onMeasure(int widthMeasureSpec, int heightMeasureSpec) @param widthMeasureSpec Horizontal space requirements as imposed by the parent @param heightMeasureSpec Vertical space requirements as imposed by the parent
onMeasure()沒有返回值,由系統呼叫。
setMeasuredDimension()用於顯式地設定寬度(width)和高度(height)值。
●MeasureSpec
MeasureSpec類封裝了從父View傳遞到子View的佈局要求。每個MeasureSpec代表對寬度或高度的要求。MeasureSpec由大小(size)和模式(mode)組成,有三種可能的模式:
MeasureSpec.EXACTLY :父View已經為View確定了一個確切的尺寸。不管子View有多大,都會給子View一個最大限制。這個屬性給View設定了一個固定的寬度(比如為LinearLayout指定weight,或者是為View指定match_parent屬性)。
MeasureSpec.AT_MOST: 子View可以根據需要變化到它想要的大小。
MeasureSpec.UNSPECIFIED:父View沒有對子View新增任何約束,子View可以是任意大小。
●onLayout()
View在呼叫onMeasure()完成測量後即會呼叫此方法,目的是在於確定View在螢幕上的位置。
●onDraw()
尺寸(通過onMeasure)和位置(通過onLayout())已經由前面的步驟計算出來了,因此View在這個時候已經可以根據上述的尺寸和位置繪製自身。在onDraw(Canvas canvas)中,生成或更新的Canvas物件具有要傳送到GPU的 OpenGL- ES命令列表(displayList)。
注意:切勿在onDraw()方法中建立物件,因為這個方法會被多次呼叫。
當特定的View的屬性發生變化時,還有另外兩個方法可以被使用,那就是:
●invalidate()
invalidate()方法用於強制重繪我們希望顯示更改的特定View。 簡單的說,如果View的外觀發生了變化,而我們又想看到這些變化,就可以呼叫invalite()。
●requestLayout()
在某些情況下,View的狀態會發生變化,requestLayout()可以向android的檢視系統發出訊號,告訴系統,View需要重新執行自身的測量和佈局階段(onMeasure()->onLayout()->onDraw())。簡單的說,當View的範圍發生變化時,我們可以呼叫此函式。
注意:在View上呼叫任何方法時,必須在UI執行緒上,如果你正在其它執行緒上工作,並且想要從該執行緒更新View的狀態,則應使用Handler。
三. State Save / Restore
●onSaveInstanceState()
要儲存檢視狀態,首先需要為其提供一個ID。如果你的檢視層級結構中有多個具有相同ID的View,則將儲存所有的狀態,為了避免這種情況,最好為View指定一個唯一的ID。
其次,你需要一個類來繼承View.BaseSavedState ,然後儲存其屬性。為了更好的理解,下面舉例說明:
●onRestoreInstanceState(Parcelable state)
在這裡我們需要重寫此方法,並從Parcelable讀取資料,然後根據Parcelable可用的資料編寫邏輯。
@Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); // The vars you want to save - in this instance a string and a boolean String someString = "something"; boolean someBoolean = true; State state = new State(super.onSaveInstanceState(), someString, someBoolean); bundle.putParcelable(State.STATE, state); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; State customViewState = (State) bundle.getParcelable(State.STATE); // The vars you saved - do whatever you want with them String someString = customViewState.getText(); boolean someBoolean = customViewState.isSomethingShowing()); super.onRestoreInstanceState(customViewState.getSuperState()); return; } // Stops a bug with the wrong state being passed to the super super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); } protected static class State extends BaseSavedState { protected static final String STATE = "YourCustomView.STATE"; private final String someText; private final boolean somethingShowing; public State(Parcelable superState, String someText, boolean somethingShowing) { super(superState); this.someText = someText; this.somethingShowing = somethingShowing; } public String getText(){ return this.someText; } public boolean isSomethingShowing(){ return this.somethingShowing; } }
<END>