1. 程式人生 > >【造輪子系列】一個選擇星期的工具——SweepSelect View

【造輪子系列】一個選擇星期的工具——SweepSelect View

簡介

首先介紹一下這個自定義View的作用,先看效果圖:
單選模式:
單選模式

多選模式:
多選模式

簡單來說,就是一個通過滑動的方式來進行選擇的工具,這種選擇方式多用於星期的選擇上,當然也是可以用於其他選項的。

構想

明確了這個View的功能後,我們再來想想應該怎麼實現呢。

  1. 先看這個View需要具有一些什麼樣的屬性:首先是待選專案;然後是字型大小和顏色,各自分為選中和未選擇兩種狀態;要能夠區分單選模式和多選模式;選中結果以後,要能將選擇的結果進行反饋;最後是背景顏色和圓角半徑可以調節。
  2. 自定義一個新的View,要讓這個view能夠正確的顯示出來,最重要的就是重寫onMeasure和onDraw方法。
  3. 要實現點選滑動的選擇效果,必須在onTouchEvent方法中進行處理。
  4. 因為每個待選項其實是相互獨立的,可以看成一個個物件,每個物件負責自己的繪製和判斷當前選中狀態。我們先設定這些物件是Item[] items;

實現

這裡我只介紹一些重要的步驟和思想,具體實現細節請移步github:SweepSelect

1. 先給View設定attribute屬性:

在res/value資料夾下新建attrs.xml檔案,在其中新增內容:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable
name="SweepSelect">
<attr name="backgroundColor" format="reference"/> <attr name="itemString" format="reference"/> <attr name="selectedColor" format="reference"/> <attr name="normalColor" format="reference"/> <attr name="selectedSize"
format="dimension"/>
<attr name="normalSize" format="dimension"/> <attr name="corner" format="dimension"/> <attr name="multyChooseMode" format="boolean"/> </declare-styleable> </resources>

這樣在使用SweepSelect的時候,就可以直接在layout檔案中進行基本的配置了,配置方法如下:

    <com.pl.sweepselect.SweepSelect
        android:id="@+id/select_week"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:backgroundColor="#515050"
        app:corner="4dp"
        app:itemString="@array/multyChooseArray"
        app:multyChooseMode="true"
        app:normalColor="#ffffff"
        app:normalSize="16sp"
        app:selectedColor="#f5c824"
        app:selectedSize="20sp" />

其中itemString那一項的內容在res/value/arrays.xml檔案中,內容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="multyChooseArray">
        <item>"週一"</item>
        <item>"週二"</item>
        <item>"週三"</item>
        <item>"週四"</item>
        <item>"週五"</item>
        <item>"週六"</item>
        <item>"週日"</item>
    </string-array>
</resources>

給SweepSelect設定好attribute屬性以後,在View中如何讀取這些屬性設定呢?接著看。

2. 重寫建構函式

一般我們新建了一個View的子類的時候,AndroidStudio都會提示我們重寫建構函式,一共有4個建構函式,分別有1個到4個引數,其中最重要的是一個引數的建構函式(以下簡稱構造1)和兩個引數的建構函式(以下簡稱構造2)。

  • 構造1往往用於在程式碼中直接new一個物件的時候呼叫;
  • 構造2是在layout中使用這個View的時候,由系統自動呼叫;

我們在layout中給View設定的attribute就是通過構造2的引數來傳遞給這個View的,所以我們應該在這裡對這些attribute進行解析,直接上程式碼:

TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.SweepSelect);
int length=typedArray.getIndexCount();
for (int i=0;i<length;i++){
    int type = typedArray.getIndex(i);
    if (type == R.styleable.SweepSelect_backgroundColor) {
        backgroundColor=typedArray.getColor(i, DEFAULT_BG_COLOR);
    }else if (type==R.styleable.SweepSelect_itemString){
        itemStrings=typedArray.getTextArray(i);
    }else if (type==R.styleable.SweepSelect_selectedColor){
        selectedColor=typedArray.getColor(i, DEFAULT_SELECTED_COLOR);
    }else if (type==R.styleable.SweepSelect_normalColor){
        normalColor=typedArray.getColor(i, DEFAULT_NORMAL_COLOR);
    }else if (type==R.styleable.SweepSelect_selectedSize){
        selectedSize=typedArray.getDimensionPixelSize(i,DEFAULT_TEXT_SIZE);
    }else if (type==R.styleable.SweepSelect_normalSize){
        normalSize=typedArray.getDimensionPixelSize(i,DEFAULT_TEXT_SIZE);
    }else if (type==R.styleable.SweepSelect_corner){
        corner=typedArray.getDimensionPixelSize(i,DEFAULT_CORNER);
    }else if (type==R.styleable.SweepSelect_multyChooseMode){
        isMultyChooseMode=typedArray.getBoolean(i,false);
    }
}
typedArray.recycle();

看程式碼應該很好理解了,R.styleable.SweepSelect_selectedColor之類的名字,就對應我們之前在attrs.xml檔案中定義的屬性,其中SweepSelect是declare-styleable中的name屬性,而selectedColor是其中的每個attr子項的name屬性。需要注意的是不同format的屬性需要用不同的函式來取值。

3. 重寫onMeasure方法

先看程式碼:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int heightMode=MeasureSpec.getMode(heightMeasureSpec);
    if (heightMode==MeasureSpec.AT_MOST){
        int atMostHeight=MeasureSpec.getSize(heightMeasureSpec);
        int height= textRect.height()*3/2+corner;
        heightMeasureSpec=MeasureSpec.makeMeasureSpec(Math.min(height,atMostHeight),heightMode);
    }else if (heightMode==MeasureSpec.UNSPECIFIED){
        int height= textRect.height()*3/2+corner;
        heightMeasureSpec=MeasureSpec.makeMeasureSpec(height,heightMode);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

我們知道onMeasure的這兩個引數其實是一個組合值,每個引數都是由mode和size組合而成的,具體的含義請查閱API文件,這裡就不詳細介紹了。當mode不同的時候,我們應該採取不同的處理。主要思路是:儘量將要展示的文字展示全,但以使用者的設定優先,所以我沒有對MeasureSpec.EXACTLY的情況做處理,也就是保持使用者所設定的高度。我也沒有對寬度進行設定,使用預設的寬度設定。
上面的程式碼中有一個變數是textRect,這是使用設定的文字和字型大小計算出來的,是文字所佔的範圍。

4.重寫onDraw方法

在onDraw中,先繪製背景,然後繪製每一個待選項item。很簡單,就不貼程式碼了,上github看吧。

5.重寫onTouchEvent方法

還是看程式碼說話:

@Override
public boolean onTouchEvent(MotionEvent event) {
    float x= event.getX();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            //防止父View搶奪觸控焦點,導致觸控事件失效
            lastX=event.getX();
            currentDirection =DIRECTION_NON;
            checkSelect(event);
            invalidate();
            return true;
        case MotionEvent.ACTION_MOVE:
            //當滑動距離小於最低限度時,視為未滑動,防止出現抖動現象
            if (Math.abs(x-lastX)<MIN_SCROLL_DISTANCE){
                return true;
            }
            if (x > lastX) {
                currentDirection = DIRECTION_RIGHT;
            } else {
                currentDirection = DIRECTION_LEFT;
            }
            checkSelect(event);
            lastX = event.getX();
            invalidate();
            return true;
        case MotionEvent.ACTION_UP:
            checkSelect(event);
            onSelectResult();
            //清理標記位
            lastX=-1;
            currentDirection =DIRECTION_NON;
            invalidate();
            return true;
    }
    return super.onTouchEvent(event);
}

需要注意的是以下幾點:
1. 當獲得按下事件的時候,應該通過呼叫getParent().requestDisallowInterceptTouchEvent(true)來防止父View爭奪焦點,這種情況多發生在巢狀入scrollView使用的情況下:手指按下的位置是在這個View中,但一旦手指移動的範圍超出View,就會收到MotionEvent.ACTION_CANCEL事件,被父View截斷觸控事件。
2. 當滑動的時候,需要有一個最小滑動距離MIN_SCROLL_DISTANCE,超過這個距離才算滑動,否則認為是手指的抖動,不應該引起選中狀態的變化。
3. 考慮一種情況,使用者按住View以後,左右滑動,這時候使用者期望的結果應該是,像效果圖中所示,根據他的滑動操作,選中狀態會有相應的變化:對於單選模式,選中使用者最後按壓的那個待選項,所以要實時重新整理選中狀態;對於多選模式,在使用者換一個方向滑動的時候,應該切換選中狀態,所以還要判斷滑動方向。

原始碼

SweepSelect
原始碼會繼續更新,部落格可能會跟不上原始碼的進度,以原始碼為準。