【輪子】自定義控件,范圍選擇器

自定義控件的時(shí)候,就想把公司項(xiàng)目里,我自己寫的自定義控件挑一個(gè)發(fā)出來,然而我懶。

寫在開頭

使用自定義控件的目的一般有兩個(gè)

  1. 實(shí)現(xiàn)特別的界面效果,若要實(shí)現(xiàn)漂亮自由的界面,靠系統(tǒng)的控件通常是很難做到的。
  2. 或者是簡化重復(fù)的無意義勞動(dòng)。頁面中一些相似但不完全相同的控件,例如一個(gè)頁面的頭部通常包含一個(gè)返回鍵,一個(gè)title,有時(shí)候還有一個(gè)右側(cè)的按鈕。雖然這種情況通過include引入一個(gè)布局文件也可以解決問題,但是每次都需要在activity中設(shè)置一次title,并且這個(gè)方法很不直觀。

自定義View的分類

  1. 繼承View
  2. 繼承ViewGroup,
  3. 繼承特定的View,比如TextView
  4. 繼承特定的ViewGroup,比如LinearLayout

繼承View或者ViewGroup,好處是更加靈活,但是有比較多的東西需要自定義,比如padding的處理。
繼承特定的View,比如TextView,或者LinearLayout,好處是實(shí)現(xiàn)起來比較快,很多地方不需要管,但是靈活性差一點(diǎn)。

自己實(shí)現(xiàn)自定義控件

本文通過一個(gè)自定義的范圍選擇器,來展示自定義控件的實(shí)現(xiàn)思路。
大概就是下面這個(gè)樣子。


一個(gè)或者兩個(gè)滑塊的選擇器.png

很簡單的控件,支持一個(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)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容