前言
對于Android程序員來說,自定義View是繞不過的話題,作為Android終端,除了一些后臺應用涝婉,大部分的應用最直接面對用戶的還是我們的界面,界面的美觀和流暢性某種程度上決定了用戶的留存糯景。
同時嘁圈,自定義View也符合封裝的思想,將通用的功能控件進行自定義彌補官方控件的使用不便蟀淮,這將提升我們的開發(fā)效率最住。
除了便利性外,當然追求各類復雜View的自定義怠惶,也是我們作為Android程序員的綜合素質的體現(xiàn)(才不是炫技呢)涨缚。
自定義View的分類
Android 官方將自定義View分為三類:
- 繼承已有控件
- 組合控件
- 完全自定義控件
繼承已有控件
如果需要實現(xiàn)的自定義View的功能與已有的控件功能類似,可以直接通過拓展已有控件的方式進行控件的自定義。
實現(xiàn)這類自定義View需要對需要繼承的已有控件的相關屬性和Api有所了解脓魏,當然兰吟,可以通過完全自定義View的方式來實現(xiàn)自定義,但是從那些已封裝的控件開始會大大提高開發(fā)的效率茂翔。
組合控件
當有一些較復雜的自定義View需要實現(xiàn)的時候混蔼,組合控件是一個較好的選擇,不會過于復雜又較為簡單珊燎,一般是繼承某一種布局來組合已有的控件惭嚣。
這種實現(xiàn)自定義View的方式最為常用,在日常的開發(fā)過程中悔政,經(jīng)常會出現(xiàn)需要封裝一些組件的需求晚吞,這時候組合控件就一個很好的選擇。
完全自定義控件
當要進行一些不規(guī)則谋国,極度復雜的控件封裝時槽地,就需要通過完全自定義的方式來實現(xiàn)View,通過實現(xiàn)提供的各種方法來實現(xiàn)芦瘾。
一般的完全自定義控件會出現(xiàn)在現(xiàn)有控件無法實現(xiàn)的情況下捌蚊,常見的是一些有著復雜形狀的UI或是需要重新定義的布局模式,才會用到完全自定義控件旅急。
自定義View的基本步驟(本文介紹前兩個步驟)
不同方式實現(xiàn)的自定義View它們的基本實現(xiàn)套路大同小異的逢勾,一般會經(jīng)過以下幾個步驟:
- 繼承已有的View或是ViewGroup,實現(xiàn)構造方法
- 自定義屬性
- 測量
- 繪制
- 處理交互事件
- 其它一些功能上的邏輯處理
本文會優(yōu)先介紹前兩個步驟藐吮,它們可以被稱為自定義View的創(chuàng)建
繼承已有的View或是ViewGroup,實現(xiàn)構造方法
構造方法是每一個類的入口逃贝,View也不例外谣辞,為了更加直觀地展現(xiàn)View的構造函數(shù),這里給出繼承自FrameLayout的構造方法(本文所用自定義View實例基于FrameLayout):
class CustomCreateView : FrameLayout {
// 第一個構造函數(shù)
constructor(context: Context) : super(context) {
}
// 第二個構造函數(shù)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
}
// 第三個構造函數(shù)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
}
// 第四個構造函數(shù)
// 在 API 21 以上才能使用
constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
}
}
一般地沐扳,通過代碼實例化一個View對象會調用第一個構造函數(shù)泥从,通過xml定義一個View會調用第二個構造函數(shù),系統(tǒng)是不會直接調用第三和第四個構造函數(shù)的沪摄,那它們有什么用呢躯嫉?
在展開構造函數(shù)之前,我們先要來了解一下自定義屬性相關的概念杨拐,也就是第三和第四個構造函數(shù)所多出來的那幾個參數(shù)內容有什么含義祈餐?
自定義屬性
View的屬性幫助我們可以快速實現(xiàn)我們所需要的功能,除了使用系統(tǒng)所提供給我們的那些View的屬性哄陶,我們還可以自定義相關的屬性帆阳,來實現(xiàn)原本所無法實現(xiàn)的功能。
實現(xiàn)步驟
定義自定義屬性
在res/values創(chuàng)建attrs.xml文件屋吨,在<declare-styleable>標簽下新建自定義屬性蜒谤,如下:
<resources>
<declare-styleable name="CustomCreateView">
<attr name="showText" format="boolean" />
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
</attr>
</declare-styleable>
</resources>
在代碼中山宾,每一個 <attr> 屬性都代表了一個自定義屬性,可以看到每個 <attr> 中都包含了兩個屬性:
- name —— 在xml引用控件設置屬性的名字
- format —— 屬性值的類型
關于format的屬性類型的設置
android在format中預設了很多屬性的類型滿足日常開發(fā)的屬性類型的需求鳍徽,下面來詳細了解一下具體有哪些屬性類型
屬性類型 | 屬性類型說明 |
---|---|
color | 顏色值资锰,一般為顏色的16進制值,例如:#000000 |
dimension | 尺寸值阶祭,用于設置控件大小或是字體大小绷杜,例如:16dp、18sp |
integer | 整形數(shù)值 |
string | 字符串類型 |
boolean | 布爾值胖翰,當屬性需要做true或false判斷的時候使用 |
enum | 枚舉類型接剩,屬性值只能選擇一個值,子項的value要設置為整形數(shù)萨咳,具體設置方法見上面的代碼 |
flags | 位或運算懊缺,屬性值可以選擇多個值,子項的value要設置為整形數(shù)培他,一般設置為2的倍數(shù)來進行區(qū)分(具體原因會在獲取屬性值的部分給出)鹃两,具體設置方法同enum,將內層標簽改為<flag> |
float | 浮點型數(shù)值 |
fraction | 百分數(shù) |
reference | 資源ID |
在這些屬性值中舀凛,enum和flags較為容易搞混俊扳,這里拿出來強調一下:
- enum —— 屬性值只能選擇一個值,例如LinearLayout的orientation屬性
- flags —— 屬性值可以選擇多個值猛遍,例如gravity屬性
另外需要注意的是format可以同時設置多個屬性值馋记,來指定多個屬性類型,常見應用就是引用資源ID和其它的屬性類型進行混合使用懊烤,多屬性類型通過“|”連接梯醒。
在 XML 布局中指定屬性值
當我們設置完自定義屬性后,就可以在對應的xml文件下的對應控件下進行使用:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.redrain.viewdemo.custom_create.CustomCreateView
android:layout_width="match_parent"
android:layout_height="match_parent"
custom:showText="true"
custom:labelPosition="left"/>
</FrameLayout>
當然這樣還不能實現(xiàn)屬性的功能腌紧,因為我們還沒有設置具體的屬性作用邏輯茸习,那么接下來就來講講如何接收設置好的自定義屬性并進行功能設置。
接收自定義屬性
還記得自定義View的構造函數(shù)嗎壁肋?其中有一個參數(shù)AttributeSet就是表示所設定的屬性号胚,也就是通過它來獲取到具體設置的值,但是直接從AttributeSet中獲取參數(shù)會導致一些問題浸遗,所以需要通過obtainStyledAttributes()方法來獲取屬性值猫胁。
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
// 通過obtainStyledAttributes方法獲取到TypedArray對象
val a = context.theme.obtainStyledAttributes(
attrs,
R.styleable.CustomCreateView,
0, 0
)
try {
// 獲取相應的屬性值
isShowText = a.getBoolean(R.styleable.CustomCreateView_showText, false)
textPos = a.getInteger(R.styleable.CustomCreateView_labelPosition, 0)
} finally {
// TypedArray是共享資源必須在使用后進行回收
a.recycle()
}
}
接受到屬性值之后就可以進行根據(jù)需求來相關的邏輯操作了。
關于各類屬性值如何獲取
每一種屬性類型都有著對應的獲取屬性值的方法:
// color的獲取
// 參數(shù)說明:index表示對應的styleable ID乙帮;defValue表示默認的顏色
int getColor(int index, int defValue)
// dimension的獲取
// 關于 dimension 的獲取杜漠,獲取到的值為px值,需要進行相應的轉化
// 參數(shù)說明:index表示對應的styleable ID;defValue表示默認的大小
float getDimension(int index, float defValue) // 獲取到像素值
int getDimensionPixelOffset(int index, int defValue) // 獲取到像素值驾茴,并轉化為整形數(shù)(取整)
int getDimensionPixelSize(int index, int defValue) // 獲取到像素值盼樟,并轉化為整形數(shù)(四舍五入的方式轉化)
// integer的獲取
// 參數(shù)說明:index表示對應的styleable ID;defValue表示默認值
// 注意:getInt方法在獲取到屬性值時锈至,如果不是整形數(shù)那么會嘗試強制轉化為int類型晨缴,而getInteger則會拋出exception
int getInt(int index, int defValue)
int getInteger(int index, int defValue)
// string的獲取
// 參數(shù)說明:index表示對應的styleable ID
String getString(int index)
// boolean的獲取
// 參數(shù)說明:index表示對應的styleable ID;defValue表示默認值
boolean getBoolean(int index, boolean defValue)
// enum的獲取
// 枚舉類型的屬性值獲取峡捡,通過getInt或是getInteger來獲取對應的屬性值击碗,獲取到的相應的值來得到枚舉的具體值
int getInt(int index, int defValue)
int getInteger(int index, int defValue)
// flags的獲取
// 通過getInt或是getInteger來獲取對應的屬性值
// 注意:由于flags類型的屬性可以設置多個屬性值,獲取到的值為所設置屬性值對應的value值之和们拙,來進一步判斷選擇了哪些屬性值
int getInt(int index, int defValue)
int getInteger(int index, int defValue)
// float的獲取
// 參數(shù)說明:index表示對應的styleable ID稍途;defValue表示默認值
float getFloat(int index, float defValue)
// fraction的獲取
// 參數(shù)說明:index表示對應的styleable ID;base表示屬性值需要乘的值砚婆;pbase表示屬性值需要除的值械拍;defValue表示默認值
float getFraction(int index, int base, int pbase, float defValue)
// reference的獲取
// 參數(shù)說明:index表示對應的styleable ID;defValue表示默認值
int getResourceId(int index, int defValue)
這里可能會有一個一個疑問了装盯,我們知道有些屬性可以通過多種方式來設置坷虑,比如text屬性可以直接通過字符串或是通過字符串引用id來設置,這種情況只需要直接通過getString方法來設置即可埂奈,Android內部已經(jīng)做了相應的邏輯處理迄损,其它類型的屬性值同理。
添加動態(tài)屬性
上述介紹的都是關于控件的靜態(tài)屬性設置账磺,有時候需要動態(tài)的對屬性進行調整芹敌,所以對需要對每一個屬性值提供一個setter&getter的方法來實現(xiàn)動態(tài)設置屬性:
var isShowText: Boolean = false
set(value) {
field = value
tvText.visibility = if (value) VISIBLE else INVISIBLE
}
當然動態(tài)屬性的設置方法不局限于上面這種,在定義動態(tài)屬性的時候需要根據(jù)需求去靈活地調整實現(xiàn)方式垮抗。
構造函數(shù)參數(shù)的含義
了解自定義屬性的相關概念党窜,那么回到我們之前講到的構造函數(shù),不同的構造函數(shù)所包含的參數(shù)都分別代表著什么呢借宵?
class CustomCreateView : View {
// 第一個構造函數(shù)
constructor(context: Context?) : super(context) {
}
// 第二個構造函數(shù)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
}
// 第三個構造函數(shù)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
}
// 第四個構造函數(shù)
// 在 API 21 以上才能使用
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
}
}
- context 上下文就不展開了,都知道這是個什么東西
- attrs 表示屬性值的集合矾削,上文中已經(jīng)講解了
- defStyleAttr
- defStyleRes
重點來講解一下后面兩個參數(shù)壤玫,有做過代碼實驗的同學可定會發(fā)現(xiàn)獲取屬性的方法中包含有兩個之前傳0的參數(shù):
val a = context.theme.obtainStyledAttributes(
attrs,
R.styleable.CustomCreateView,
0, 0
)
最后兩個參數(shù)分別代表了defStyleAttr
和defStyleRes
。
defStyleAttr
相當于為View設置一個主題風格的屬性配置哼凯,如果沒有在xml中沒有定義相關的屬性欲间,但又在主題中定義了相關屬性,那么會從defStyleAttr
所指向的style中查找對應的屬性断部。
可以這么理解它猎贴,它依賴于主題,不同主題中定義不同的defStyleAttr
,實現(xiàn)的效果也不同她渴。
defStyleRes
指向Style的資源ID达址,但是僅在defStyleAttr
為0或者defStyleAttr
不為0但Theme中沒有為defStyleAttr
屬性賦值時起作用。
簡單來說就是這種方式和主題無關趁耗,它就是兜底的默認屬性風格沉唠。
具體傳參使用方式
defStyleAttr
首先,要在values/attrs
中定義特定的屬性名:
<attr name="CustomCreateViewDefStyleAttr" format="reference"/>
然后在values/styles
中定義所要設置的屬性風格:
<style name="CustomCreateViewLeftStyleAttr">
<item name="showText">true</item>
<item name="labelPosition">left</item>
</style>
下一步苛败,在所需要定義的style中定義對應的屬性(本文這里就偷懶直接放在AppTheme上了):
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="CustomCreateViewDefStyleAttr">@style/CustomCreateViewLeftStyleAttr</item>
</style>
最后在構造函數(shù)中定義默認的屬性風格:
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, R.attr.CustomCreateViewDefStyleAttr) {
Log.d("customCreate", "第二個構造方法")
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
Log.d("customCreate", "第三個構造方法")
val view = LayoutInflater.from(context).inflate(R.layout.view_custom_create, this)
tvText = view.findViewById(R.id.tv_text)
tvText.text = "CustomCreateView"
// 通過obtainStyledAttributes方法獲取到TypedArray對象
val a = context.obtainStyledAttributes(
attrs,
R.styleable.CustomCreateView,
defStyleAttr, 0
)
try {
// 獲取相應的屬性值
isShowText = a.getBoolean(R.styleable.CustomCreateView_showText, false)
labelPosition = a.getInteger(R.styleable.CustomCreateView_labelPosition, 0)
} finally {
// TypedArray是共享資源必須在使用后進行回收
a.recycle()
}
tvText.visibility = if (isShowText) VISIBLE else INVISIBLE
tvText.gravity = if (labelPosition == 0) Gravity.LEFT else Gravity.RIGHT
}
注意這種方式的屬性定義和一般的屬性定義的區(qū)別在于:
- 通過二參構造函數(shù)實現(xiàn)三參構造函數(shù)满葛,第三個參數(shù)定義為我們定義好的資源id
-
obtainStyledAttributes
方法中的第三個參數(shù)傳入defStyleAttr
這樣就實現(xiàn)了defStyleAttr
方式的屬性定義,這種方式實際上會到theme中去尋找定義好的特定屬性名罢屈,如果有嘀韧,則會在優(yōu)先級允許的情況下,使用特定屬性名下的定義屬性缠捌,如果沒有則不會去獲取屬性锄贷。
defStyleRes
defStyleRes
實現(xiàn)只需要在defStyleAttr
實現(xiàn)上稍微做一點小改動。
去掉theme中的定義:
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
讓三參構造函數(shù)實現(xiàn)四參構造函數(shù):
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, R.attr.CustomCreateViewDefStyleAttr) {
Log.d("customCreate", "第二個構造方法")
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : this(
context,
attrs,
defStyleAttr,
R.style.CustomCreateViewLeftStyleAttr
) {
Log.d("customCreate", "第三個構造方法")
}
constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
Log.d("customCreate", "第四個構造方法")
val view = LayoutInflater.from(context).inflate(R.layout.view_custom_create, this)
tvText = view.findViewById(R.id.tv_text)
tvText.text = "CustomCreateView"
// 通過obtainStyledAttributes方法獲取到TypedArray對象
val a = context.obtainStyledAttributes(
attrs,
R.styleable.CustomCreateView,
defStyleAttr, defStyleRes
)
try {
// 獲取相應的屬性值
isShowText = a.getBoolean(R.styleable.CustomCreateView_showText, false)
labelPosition = a.getInteger(R.styleable.CustomCreateView_labelPosition, 0)
} finally {
// TypedArray是共享資源必須在使用后進行回收
a.recycle()
}
tvText.visibility = if (isShowText) VISIBLE else INVISIBLE
tvText.gravity = if (labelPosition == 0) Gravity.LEFT else Gravity.RIGHT
}
注意這種方式的屬性定義和一般的屬性定義的區(qū)別在于:
- 通過三參構造函數(shù)實現(xiàn)四參構造函數(shù)鄙币,第四個參數(shù)定義為我們定義好的資源id
-
obtainStyledAttributes
方法中的第四個參數(shù)傳入defStyleRes
由于在主題中肃叶,已經(jīng)去掉了defStyleAttr
方式定義的屬性資源,那么最終會調用defStyleRes
所定義的屬性資源十嘿,也就是:
<style name="CustomCreateViewLeftStyleAttr">
<item name="showText">true</item>
<item name="labelPosition">right</item>
</style>
屬性設置區(qū)別
上文中已經(jīng)將各類的屬性賦值一一講解因惭,那么它們區(qū)別以及應用場景是什么呢?
屬性設置:
- 在布局xml中直接定義
- 在布局xml中通過style定義
- 自定義View所在的Activity的Theme中指定style引用
- 構造函數(shù)中defStyleRes指定的默認值
屬性設置的方式有以上幾種绩衷,優(yōu)先級從高到低(如果高優(yōu)先級的定義了蹦魔,那么低優(yōu)先級的就不會采用),下文中使用數(shù)字來表示:
- 很好理解咳燕,xml的屬性定義是程序員的第一設置項勿决,反映了程序員的期望。
- style定義可以理解為某一種設計的規(guī)范招盲,增強了復用性低缩,但其可以被1所定義的屬性替換,在規(guī)范的同時曹货,提升定制的可能咆繁。
- 更像一種主題性質的定義,統(tǒng)一的主題定義顶籽,方便了程序員來定制整體樣式玩般。
- 提供了最基礎的默認值,保證了整體風格的統(tǒng)一礼饱。
Android將屬性定義通過不同顆粒度的定義方式進行了區(qū)分坏为,最主要的還是幫助開發(fā)者提高開發(fā)的效率究驴。
總結
本文主要講解自定義View的創(chuàng)建過程,從構造函數(shù)這個入口作為切入點匀伏,講解自定義屬性的定義過程和不同方式下對View的屬性風格的把握洒忧,本文沒有給出完整的源碼,但是所使用的代碼是連貫的帘撰,建議想嘗試的同學可以自己寫一寫代碼做一下嘗試跑慕,尤其是defStyleAttr
和defStyleRes
的概念一定要好好地理解一下,實際開發(fā)中會極大地提高我們的開發(fā)效率摧找。