寫自定義控件的時(shí)候,就想把公司項(xiàng)目里,我自己寫的自定義控件挑一個(gè)發(fā)出來,然而我懶。
寫在開頭
使用自定義控件的目的一般有兩個(gè)
- 實(shí)現(xiàn)特別的界面效果,若要實(shí)現(xiàn)漂亮自由的界面,靠系統(tǒng)的控件通常是很難做到的。
- 或者是簡化重復(fù)的無意義勞動(dòng)。頁面中一些相似但不完全相同的控件,例如一個(gè)頁面的頭部通常包含一個(gè)返回鍵,一個(gè)title,有時(shí)候還有一個(gè)右側(cè)的按鈕。雖然這種情況通過include引入一個(gè)布局文件也可以解決問題,但是每次都需要在activity中設(shè)置一次title,并且這個(gè)方法很不直觀。
自定義View的分類
- 繼承View
- 繼承ViewGroup,
- 繼承特定的View,比如TextView
- 繼承特定的ViewGroup,比如LinearLayout
繼承View或者ViewGroup,好處是更加靈活,但是有比較多的東西需要自定義,比如padding的處理。
繼承特定的View,比如TextView,或者LinearLayout,好處是實(shí)現(xiàn)起來比較快,很多地方不需要管,但是靈活性差一點(diǎn)。
自己實(shí)現(xiàn)自定義控件
本文通過一個(gè)自定義的范圍選擇器,來展示自定義控件的實(shí)現(xiàn)思路。
大概就是下面這個(gè)樣子。

很簡單的控件,支持一個(gè)滑塊或者兩個(gè)滑塊。
是不是真的需要自定義
在寫自定義控件的時(shí)候,首先要確定的是,這個(gè)控件是否真的需要自己實(shí)現(xiàn)。
畢竟android自帶控件是經(jīng)過時(shí)間與性能的考驗(yàn)的,TextView動(dòng)輒一萬來行,性能與功能都可以得到保障。
一般的動(dòng)畫需求,通過屬性動(dòng)畫加上系統(tǒng)控件都可以得到實(shí)現(xiàn)。
但是難保碰到一個(gè)腦洞大開的產(chǎn)品非要讓你根據(jù)手機(jī)殼動(dòng)態(tài)切換主題。那就沒辦法,自己寫嘍。
三個(gè)構(gòu)造函數(shù)
自定義控件時(shí),系統(tǒng)會(huì)為我們生成3個(gè)構(gòu)造函數(shù),對(duì)應(yīng)本文中的
public RangeSelectBar(Context context){}
public RangeSelectBar(Context context, @Nullable AttributeSet attrs){}
public RangeSelectBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr){}
區(qū)別是
如果在代碼中,通過new來實(shí)例化一個(gè)控件,就是調(diào)用第一個(gè)構(gòu)造函數(shù)
如果在布局文件中,添加一個(gè)控件,就會(huì)調(diào)用第二個(gè)構(gòu)造函數(shù)
而第三個(gè)構(gòu)造函數(shù)不常用,它可以由我們自己調(diào)用,并且傳入一個(gè)style
我比較喜歡通過this函數(shù),在第一個(gè)構(gòu)造中調(diào)用第二個(gè),在第二個(gè)構(gòu)造中調(diào)用第三個(gè),或者看情況忽視第三個(gè)。
自定義屬性
通過自定義屬性可以讓我們的自定義控件更具有靈活性與實(shí)用性,而非一次性的產(chǎn)品。
聲明
首先是聲明自定義屬性。
在res-values文件夾下新建一個(gè)Value resource file,命名為attrs.xml
接下來,按照以下格式往文件里寫入自定義屬性規(guī)則
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="自定義屬性組名">
<attr name="屬性名,會(huì)展示在XML文件中" format="該屬性允許接接收的值的類型" /><!--注釋-->
</declare-styleable>
</resources>
在當(dāng)前項(xiàng)目中,使用了以下幾種格式
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RangeSelectBar">
<attr name="RSBSelectColor" format="color" /><!--選中的范圍顏色。-->
<attr name="RSBHeight" format="dimension" /><!--進(jìn)度條高度-->
<attr name="RSBSliderRes" format="reference|color" /><!--滑塊資源id,左右兩邊相同。-->
<attr name="RSBLeftSliderPosition" format="integer" /><!--左方滑塊位置-->
<attr name="RSBHideLeftSlide" format="boolean" /><!-- 隱藏左側(cè)滑塊-->
</declare-styleable>
</resources>
其中
color代表可以使用顏色id或者色值
dimension可以填入長度數(shù)字,dp,px之類
reference代表圖片或者shape資源文件
integer代表整形數(shù)值
boolean代表布爾類型的數(shù)值
此外,比較常用的還有string字符型,以及float浮點(diǎn)型
并且接受的屬性類型,允許同時(shí)接收兩種或者兩種以上的類型,比如可接受資源文件的屬性一般會(huì)兼容顏色類型,就是代碼中的reference|color
源碼中,各個(gè)view的background屬性以及imageview的src屬性便是這樣。
使用
聲明完成之后便是使用了,一般聲明完成的屬性,便可以在xml中直接敲出來,但是需要添加命名空間“app”,自定義屬性使用時(shí),也是需要使用app這個(gè)命名空間而非系統(tǒng)控件中的android命名空間。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
...
>
<com.zx.rangselectbar.RangeSelectBar
...
app:RSBBackgroundColor="@color/gray_word_light"
...
/>
</LinearLayout>
不過這個(gè)命名空間不需要手寫,在xml中寫入自定義屬性,系統(tǒng)便會(huì)提示添加命名空間了。
讀取
最后也是最重要的,就是讀取自定義屬性值了
系統(tǒng)讀取到布局文件中的屬性以及屬性值以后,會(huì)通過第二個(gè)構(gòu)造函數(shù)中的第二個(gè)屬性“attrs”將該集合傳入。
TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.RangeSelectBar);
barHeight = t.getDimension(R.styleable.RangeSelectBar_RSBHeight, 4);
barSelectColor = t.getColor(R.styleable.RangeSelectBar_RSBSelectColor, getResources().getColor(R.color.colorPrimary));
sliderDrawable = t.getDrawable(R.styleable.RangeSelectBar_RSBSliderRes);
leftSliderPosition = t.getInt(R.styleable.RangeSelectBar_RSBLeftSliderPosition, 0);
hideLeftSlider = t.getBoolean(R.styleable.RangeSelectBar_RSBHideLeftSlide, false);
// 最后記得回收
t.recycle();
系統(tǒng)通過集合的形式來整合布局文件中的屬性以及屬性的值,其中,如果需要同時(shí)接收reference|color的值,一般使用Drawable來接受該屬性。
這個(gè)過程一般會(huì)在構(gòu)造階段完成。
View生命周期的三個(gè)函數(shù)
在自定義控件一文中詳細(xì)寫過這三個(gè)函數(shù)的作用,這里在總結(jié)一下。
measure測量當(dāng)前控件需要的空間大小
layout對(duì)當(dāng)前控件進(jìn)行布局
draw將布局完成的控件呈現(xiàn)出來
onMeasure
我們知道,當(dāng)不進(jìn)行特殊處理時(shí),布局文件中wrap與match屬性所呈現(xiàn)的效果都是一致的,都會(huì)填充父布局,為了不讓我們的控件像是一個(gè)不知道自己飯量的傻子,簡單的處理是必須的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightSpecMode == MeasureSpec.AT_MOST) {
heightSpecSize = (int) (lagerBetweenTheTwo + barBottomPadding + textSize + getPaddingTop() + getPaddingBottom());
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
繼承View的自定義控件,自身最小需要多少空間非常好算
本文中,范圍選擇器所需的最小高度就是
軸體/滑塊比較寬的那一個(gè)+文字大小+兩者間隙+上下間距。
而手機(jī)的顯示器的寬比較短,寬度填充父容器才能得到比較好的顯示效果,所以不進(jìn)行處理。
onLayout
一般來說,自定義控件是不需要處理onLayout的,onlayout方法多用于父控件對(duì)子控件的布局控制。
但是本文中的自定義控件,雖說是繼承于View,并且沒有機(jī)會(huì)對(duì)其添加子控件,但是它自身卻是由多個(gè)部分組成的,所以接下來需要處理布局。
此時(shí),控件被測量完畢,開始計(jì)算各個(gè)部分的放置位置??丶偣灿扇糠纸M成,從上到下依次是
- 控件主體,是長條加滑塊的一個(gè)組合,取其中高度最高者
- 控件主體與下方文字間隙的高度
- 下方文字高度
然后按照事先想好的規(guī)則,就像拼積木一樣將每個(gè)部分放上去,計(jì)算四角的坐標(biāo)就可以了。
這段代碼對(duì)講解意義不大就不貼出來了。
onDraw
onLayout 部分已經(jīng)計(jì)算好了每個(gè)部分的位置,最后一步只要將其畫出來即可。
當(dāng)然本例中,會(huì)在onDraw中計(jì)算滑塊的四角坐標(biāo),因?yàn)榛瑝K式隨著手指移動(dòng)的,而每次手指觸摸滑塊時(shí),會(huì)實(shí)時(shí)計(jì)算滑塊的坐標(biāo),并且通過invalidate方法對(duì)圖形進(jìn)行刷新,這樣的話,在onDraw方法中,通過滑塊中心點(diǎn)統(tǒng)一計(jì)算滑塊的坐標(biāo)反而是比較節(jié)省計(jì)算時(shí)間的一個(gè)方式。代碼也更加簡潔。
onTouch
當(dāng)然對(duì)于一部分同學(xué)來說,如何在自定義控件中處理觸摸事件也是一件比較頭疼的事情,但這個(gè)不是自定義控件的難點(diǎn)所在,希望事件分發(fā)機(jī)制會(huì)對(duì)你有幫助。
以及最重要的
最重要的源碼地址,歡迎下載,覺得有用請(qǐng)給我個(gè)star。
個(gè)人理解,難免有錯(cuò)誤紕漏,歡迎指正。轉(zhuǎn)載請(qǐng)注明出處。