Android 中最讓人感到興奮的就是看到GitHub上各種自定義View的實現了,而自定義View對自己個人而言是那種痛並快樂的一件事情,一旦次數多了,不痛了也就能夠感受到自定義View的所帶來的快樂了,
但是自己坦白還沒到那種水平,自定義View有一定的固定套路,也有一部分很靈活的部分,而最重要的部分恰恰是后者,該博文將只會介紹Android控件的整體架構,自定義View固定流程,后續將會另起一個專題專門將工作這段時間所實現的一些比較好玩的自定義View。
在開始本篇博文之前,需要介紹下Android 控件的整體架構,這樣對整個流程的介紹會更清晰:
Android 控件的整體架構
Android中的所有可視化組件都是從View派生出來的,這些可視化組件通常被稱為控件或者小組件,ViewGroup類是對View類的擴展,它用來包含多個視圖,ViewGroup主要用於管理子視圖布局和創建可重用的復合組件。它是View的容器,可以存放View 還可以存放ViewGroup,從而形成樹狀結構。
每個控件樹都有一個ViewParent,它負責所有交互事件的調度和分配,並且順着控件樹,上層的控件負責下層空間的測量與繪制,每個View占據屏幕上的一個矩形區域,每個View對象負責這個矩形區域的測度、布局、繪圖、焦點改變、滾動、觸摸,手勢等交互事件的處理。
根節點要求它的孩子節點繪制它自己,每個ViewGroup節點要求調用自己的子視圖繪制自己。子視圖可以往父視圖中請求指定的尺寸數據,但是每個子視圖的父視圖對象對子視圖的尺寸有最終決定權,也就是說子視圖可以告訴乎視圖要求多大的空間,但是父視圖會縱觀全局來決定實際分配給每個子視圖的大小。如果元素有重疊的地方,重疊部分后面繪制的將在之前繪制的上面。經過上述過程至頂向下的繪制最終繪制出整個頁面;
每個Activity都持有一個Window對象,它是一個抽象類,它有一個子類PhoneWindow是Window類的具體實現,可以通過PhoneWindow具體去繪制窗口。
DecorView 是PhoneWindow類的內部類,它是窗口界面的頂層視圖,是所有應用窗口的根View。大部分事件都是由它傳遞給view,一般情況下它有上下兩部分組成,它將要顯示的內容呈現在PhoneWindow上。如下圖所示,一個是TitleView,一個是ContentView,我們可以通過setContentView來將布局添加到ContentView中。
View以及ViewGroup的測量
為什么需要測量,這是因為在繪制一個View的時候不僅需要知道它的形狀等信息,還需要知道它的大小信息。View的繪制工作在onMeasure方法中進行,通過這個方法可以指定該控件在屏幕上的大小,重寫該方法時需要計算控件的實際大小,然后調用setMeasuredDimension(int, int)將確定尺寸數值設置為控件的實際大小。
onMeasure被調用的時候傳入兩個參數widthMeasureSpec和heightMeasureSpec。每個都是一個32位的int值它分成兩個部分高兩位為測量模式,低30位為測量的大小。
我們可以通過:
int mode = MeasureSpec.getMode(xxxxxxx)獲取到模式,
int size = MeasureSpec.getSize(xxxxxxx)獲取到尺寸數值。
測量模式有三種:
EXACTLY:精確模式,當我們將layout_height以及layout_width設置為具體數值時,或者設置為match_parent的時候,使用的是這種模式
AT_MOST:最大值模式,當控件的layout_height以及layout_width被設置為wrap_content屬性的時候,使用的是這種模式,這種模式下控件的尺寸只要不超過父控件允許的最大尺寸即可。
UNSPECIFIED:這種情況我見得不是很多,基本上沒有用到。
View 默認情況下支持EXACTLY模式,因此如果不重寫onMeasure方法時,只能使用EXACTLY模式,所定義的View只能指定具體尺寸,或者是match_parent,而不能是wrap_content.如果需要支持wrap_content就必須重寫onMeasure方法。
@Override |
View的繪制:
我們知道自定義一個View需要繼承自View並重寫構造方法以及onDraw方法。
-
覆寫構造方法
一般我們在Android Studio創建一個View的時候,會要求復寫構造方法,默認情況下會有三個構造函數:
public void CustomView(Context context) {} |
第一個構造函數用在代碼中動態創建對象時使用的,如果只打算在代碼中動態創建一個view而不使用布局文件xml,那么就直接實現這個構造方法就可以了,但是一般都不這么做,因為難保證后面不會用在layout布局中,雖然后面使用的時候添加一個也很容易,但是一般項目代碼都很龐大
。如果沒有遵守這個規則的話,后面誰來在你的平台上開發的話,將其用在XML布局文件中,就會引起問題,雖然這種錯誤很好排查,但是如果遇到這種情況一般都會問候下原先維護這個代碼的那個人。所以如果不想在離職后還被別人掛念還是繼續實現后面兩個構造方法吧。
第二個構造方法比第一個構造方法多了一個AttributeSet類型的參數,通過布局文件xml創建一個view時,這個參數會將xml里設定的屬性傳遞給構造函數。如果采用xml方法卻沒有實現這種構造方法,那么雖然編譯的時候會順利通過但是運行時就會報錯。
第三個構造方法比第二個構造方法多了一個defStyle的參數,這個參數用來指定view的默認style,如果為0將不會應用任何默認的style。那么這個值又是從哪里傳過來的呢?
一般這個系統是不調用的,一般用於提供給第二個構造方法使用的,在第二個構造方法中會傳給第三個構造方法一個默認的style id。
public class CustomView extends View { |
-
自定義屬性
- 定義屬性
在res/values/attrs.xml文件中為添加自定義的屬性的定義
<resources> |
這里需要了解下屬性的類型有哪些:
-
reference:引用某一資源ID。
定義:
<attr name = "background" format = "reference" />
屬性使用:
android:background = "@drawable/圖片ID" -
color:顏色值。
定義:
<attr name = "textColor" format = "color" />
屬性使用:
android:textColor = "#00FF00" -
boolean:布爾值。
定義:
<attr name = "focusable" format = "boolean" />
屬性使用:
android:focusable = "true"-
dimension:尺寸值。
定義:
<attr name = "layout_width" format = "dimension" />
屬性使用:
android:layout_width = "42dip" -
float:浮點值。
定義:
<attr name = "fromAlpha" format = "float" />
屬性使用:
android:fromAlpha = "1.0" -
integer:整型值。
定義:
<attr name = "framesCount" format="integer" />
屬性使用:
android:frameDuration = "100" -
string:字符串。
定義:
<attr name = "apiKey" format = "string" />
屬性使用:
android:apiKey = "0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g" -
fraction:百分數。
定義:
<attr name = "pivotX" format = "fraction" />
屬性使用:
android:pivotY = "300%" -
enum:枚舉值。
定義:
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
屬性使用:
android:orientation = "vertical" -
flag:位或運算。
定義:
<attr name="windowSoftInputMode">
<flag name = "stateUnspecified" value = "0" />
<flag name = "stateUnchanged" value = "1" />
<flag name = "stateHidden" value = "2" />
<flag name = "stateAlwaysHidden" value = "3" />
<flag name = "stateVisible" value = "4" />
<flag name = "stateAlwaysVisible" value = "5" />
<flag name = "adjustUnspecified" value = "0x00" />
<flag name = "adjustResize" value = "0x10" />
<flag name = "adjustPan" value = "0x20" />
<flag name = "adjustNothing" value = "0x30" />
</attr>
屬性使用:
android:windowSoftInputMode = "stateUnspecified | stateUnchanged | stateHidden">
-
屬性定義時可以指定多種類型值。
<attr name = "background" format = "reference|color" /> |
2.在xml中為相應的屬性聲明屬性值
- 直接在layout中使用屬性:這個就不做介紹了,一般布局很多都是這種方式 custom:attr_1=”attr one”
-
設置style並在style中設置屬性
<resources>
<style name="DirectStyle">
<item name="attr_1">attr one from DirectStyle</item>
<item name="attr_2">attr two from DirectStyle</item>
</style>
</resources>
使用方式:
style="@style/DirectStyle" -
在主題中指定在當前Application或Activity中屬性的默認值
<style name="AppTheme" parent="AppBaseTheme">
<item name="attr_1">attr one from Theme</item>
<item name="CustomizeStyleRef">@style/CustomizeStyle</item>
</style> -
在defStyle提供默認值
它們的順序如下:
XML中定義>style定義>由defStyle提供默認值>在Theme中指定的值 |
下面是個人實現自定義View為View添加屬性時候的固定步驟:
<resources> |
-
如何獲得屬性值
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.CustomizeStyleRef);//注意這里是從attr獲取
}
public CustomTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Customize, defStyle,R.style.DefaultCustomizeStyle);
try {
attr_1 = a.getString(R.styleable.Customize_attr_1);//注意這里!!!!!格式是定義屬性名_屬性名
} finally {
a.recycle();//TypedArry是一個共享的資源,使用完畢必須回收它。
}
...
} -
覆寫onDraw方法繪制View
[見繪圖部分]
-
覆寫onTouch等事件相應方法
[見事件部分] -
設置控件的回調接口
自定義View步驟總結:
- 繼承View或繼承View的子類
- 在res/values/attrs.xm 中新增節點定義自定義屬性
-
將自定義View放到布局文件中,注意命名空間名的格式為http://schemas.android.com/apk/res/[自定義View所在的包路徑] 比如
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
<com.example.customviews.CustomView
custom:attr_1="dddddddddddd" />
</LinearLayout> -
在XML布局中設定指定屬性的值
- 在CustomView 中獲取對應的屬性值並覆寫構造方法以及onDraw方法
- 覆寫onTouch等事件相應方法
自定義View過程中除了上述介紹的onMeasue方法還有如下重要的回調方法:
onFinishInflate() 從XML加載組件后調用
onMeasure() 調用該方法進行測量
onLayout() 調用該方法確定顯示的位置
onSizeChange() 組件大小改變的時候這個方法會被掉用
onTouchEvent 當觸摸事件來臨時被調用
自定義View是一個需要很長時間實踐才能掌握的技術,上面進階是一些死東西,活的東西需要在實踐中不斷積累,多看別人的作品,並多實踐是掌握自定義View的不二法則,下面是一些我之前收集的一些較好的博文,推薦給大家。
[推薦博文]
blog.csdn.net/jdsjlzx/article/details/41113969
http://blog.csdn.net/lmj623565791/article/details/24252901
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0606/3001.html
http://blog.csdn.net/feelang/article/details/45035759
http://www.zhihu.com/question/41101031
http://blog.csdn.net/singwhatiwanna/article/details/38168103
http://blog.csdn.net/aigestudio/article/details/41447349