認識一個新的事物,首先我們從概念上講,我們需要知道,這個事物 是什麼,這個事物有什麼用途?
對應到自定義View 上,首先我們要搞明白 View 的定義以及工作原理。
1.View是什麼?
View是屏幕上的一塊矩形區域,它負責用來顯示一個區域,並且響應這個區域內的事件。可以說,手機屏幕上的任意一部分看的見得地方都是View,它很常見,比如 TextView 、ImageView 、Button以及LinearLayout、RelativeLayout都是繼承子View的。
對於Activity來說,我們通過setContentView(view)添加的佈局到Activity上,實際上都是添加到了Activity 內部的DecorView上面,這個DecorView,其實就是一個FrameLayout,因此實際上,我們的佈局實際上添加到了FrameLayout裏面。
2.View 是怎麼工作的?
View的工作流程分爲兩部分,第一部分 顯示在屏幕上的過程, 第二部分 響應屏幕上的觸摸事件的過程。
對於顯示在屏幕上的過程:是View 從無到有,經過測量大小(Measure)、確定在屏幕中的位置(Layout)、以及最終繪製在屏幕上(Draw) 這一系列的過程。
對於響應屏幕上的觸摸事件的過程:則是Touch事件的分發過程(這一部分,在這裏先不涉及)。
Measure() Layout()方法是final修飾的,無法重寫 ,Draw()雖然不是final的,但是也不建議重寫該方法。
3.如何自定義View?
View 爲我們提供了 onMeasure() onLayout() onDraw() 這樣的方法,其實所謂的自定義View,也就是對onMeasure() onLayout() onDraw() 三個方法的重寫的過程。
所謂的自定義View 實際上,就是仿照View的工作流程,去自己實現的過程。根據繼承對象的不同自定義View又分爲繼承View 與ViewGroup兩種。
measure():
View的onMeasure()方法如下:
內部調用了 setMeasuredDimension() 方法,這個方法看名字就知道是設置測量的View的寬/高,傳遞的參數就是寬度和高度。
onMeasure通過父View傳遞過來的大小和模式,以及自身的背景圖片的大小得出自身最終的大小,通過setMeasuredDimension()方法設置給mMeasuredWidth和mMeasuredHeight。
普通View的onMeasure邏輯大同小異,基本都是測量自身內容和背景,然後根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,文字的大小,文字行高,文字的行寬,顯示方式,背景圖片,以及父View傳遞過來的模式和大小最終確定自身的大小。
ViewGroup本身沒有實現onMeasure,但是他的子類都有各自的實現,通常他們都是通過measureChildWithMargins函數或者其他類似於measureChild的函數來遍歷測量子View,被GONE的子View將不參與測量,當所有的子View都測量完畢後,才根據父View傳遞過來的模式和大小來最終決定自身的大小。
ViewGroup一般都在測量完所有子View後纔會調用setMeasuredDimension()設置自身大小。
這裏我們就要介紹一下Android中自己定義的測量規格:MeasureSpec。
因爲測量過程中,系統會把我們代碼或者佈局中設置的View的LayoutParams 轉換成MeasureSpec,然後通過MeasureSpec來測量確定View的寬高。
MeasureSpec由32位 int值組成,高2位表示的是測量模式(specMode),後面的30位代表的是測量的大小(specSize)。
MeasureSpec是由父佈局與View 自身的LayoutParams來決定的
經過measure 完成後,我們就可以通過getMeasuredWidth/Height 獲取View 的寬高。
layout()
Layout() 方法如果是ViewGroup,則循環遍歷所有子View,普通View則空實現,因此如果我們繼承ViewGroup 我們需要遍歷執行所有的child.layout()。
Layout方法中接受四個參數,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據子View在measure中測量的大小來決定。
子View的位置通常還受有其他屬性左右,例如父View的orientation,gravity,自身的margin等等,影響佈局的因素非常多。
onLayout是ViewGroup用來決定子View擺放位置的,各種佈局的差異都在該方法中得到了體現。
onLayout比layout多一個參數,changed,該參數是在setFrame通過比對上次的位置得出是否發生了變化,通常該參數沒有被使用的意義,因爲父View位置和大小不變,並不能代表子View的位置和大小沒有發生改變。
draw()
draw()的過程就是繪製View到屏幕上的過程,draw()的執行遵循如下步驟:
一般2和5 是可以跳過的。
在TextView中在該方法中繪製文字、光標和CompoundDrawable 、ImageView中相對簡單,只是繪製了圖片。
View 的繪製主要通過dispatchDraw()
先根據自身的padding剪裁畫布,所有的子View都將在畫布剪裁後的區域繪製。
遍歷所有子View,調用子View的computeScroll對子View的滾動值進行計算。
根據滾動值和子View在父View中的座標進行畫布原點座標的移動,根據子在父View中的座標計算出子View的視圖大小,然後對畫布進行剪裁,請看下面的示意圖。
dispatchDraw的邏輯其實比較複雜,但是幸運的是對子View流程都採用該方式,而ViewGroup已經處理好了,我們不必要重載該方法對子View進行繪製事件的派遣分發。
理解了以上工作流程,就可以試着去自定義View了。
自定義View一般就分爲 繼承View/VieGroup兩種,在我們自定義View的時候要讓自定義的View的功能儘可能向
系統提供的考慮,比如支持wrap_content,padding 等。
View 的幾個比較重要的方法:
requestLayout:
方法會執行,區別就是Invalidate不能直接在線程中調用,因爲他是違背了單線程模型:Android
UI操作並不是線程安全的,並且這些操作必須在UI線程中調用。 鑑於此,如果要使用invalidate的刷新,那我們就得配合handler的使用,使異步非ui線程轉到ui線程中調用,如果要在非ui線程中直接使用就調用postInvalidate方法即可,這樣就省去使用handler的煩惱。