1. 程式人生 > >Android平臺一款UI體驗好於NumberPicker的自定義控制元件NumberPickerView

Android平臺一款UI體驗好於NumberPicker的自定義控制元件NumberPickerView

NumberPickerView

another NumberPicker with more flexible attributes on Android platform

專案地址

前言

在平時開發中會用到NumberPicker元件,但是預設風格的NumberPicker具有一些不靈活的屬性,且定製起來比較麻煩,且缺少一些過渡動效,因此在應用開發時,一般採用自定義的控制元件來完成選擇功能。

控制元件截圖


這裡寫圖片描述
靜態截圖以及漸變效果

這裡寫圖片描述
應用NumberPickerView的一個例項,一款可以選擇公曆/農曆日期的View,且公農曆自由切換,專案地址 GregorianLunarCalendar


這裡寫圖片描述
應用NumberPickerView的一個例項,一款可以選擇公曆/農曆日期的View,且公農曆自由切換

說明

NumberPickerView是一款與android原生NumberPicker具有類似介面以及類似功能的View
主要功能同樣是從多個候選項中通過上下滾動的方式選擇需要的選項,但是與NumberPicker相比較,有幾個主要不同點,下面是兩者的不同之處。

原始控制元件特性-NumberPicker

  1. 顯示視窗只能顯示3個備選選項;
  2. 在fling時阻力較大,無法快速滑動;
  3. 在選中與非選中狀態切換比較生硬;
  4. 批量改變選項中的內容時,沒有動畫效果;
  5. 動態設定wrap模式時(setWrapSelectorWheel()方法),會有“暫時顯示不出部分選項”的問題;
  6. 選中位置沒有文字說明;
  7. 程式碼中不能控制選項滑動滾動到某一item;

自定義控制元件特性-NumberPickerView

  1. 顯示視窗可以顯示多個備選選項;
  2. fling時滑動速度較快,且可以設定摩擦力;
  3. 在選中與非選中的狀態滑動時,具有漸變的動畫效果,包括文字放大縮小以及顏色的漸變;
  4. 在批量改變選項中的內容時,可以選擇是否採用友好的滑動效果;
  5. 可以動態的設定是否wrap,即,是否迴圈滾動;
  6. 選中位置可以新增文字說明,可控制文字字型大小顏色等;
  7. 具有在程式碼中動態的滑動到某一位置的功能;
  8. 支援wrap_content,支援item的padding
  9. 提供多種屬性,優化UI效果
  10. 在滑動過程中不響應onValueChanged()
  11. 點選上下單元格,可以自動滑動到對應的點選物件。
  12. 相容NumberPicker的重要方法和介面:
    相容的方法有:
    setOnValueChangedListener()
    setOnScrollListener()
    setDisplayedValues()/getDisplayedValues()
    setWrapSelectorWheel()/getWrapSelectorWheel()
    setMinValue()/getMinValue()
    setMaxValue()/getMaxValue()
    setValue()/getValue()

    相容的內部介面有:
    OnValueChangeListener
    OnScrollListener

使用方法

1.匯入至工程

    compile 'cn.carbswang.android:NumberPickerView:1.0.2'

或者

    <dependency>
      <groupId>cn.carbswang.android</groupId>
      <artifactId>NumberPickerView</artifactId>
      <version>1.0.2</version>
      <type>pom</type>
    </dependency>

2.通過佈局宣告NumberPickerView

    <cn.carbswang.android.numberpickerview.library.NumberPickerView
        android:id="@+id/picker"
        android:layout_width="wrap_content"
        android:layout_height="240dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:background="#11333333"
        android:contentDescription="test_number_picker_view"
        app:npv_ItemPaddingHorizental="5dp"
        app:npv_ItemPaddingVertical="5dp"
        app:npv_ShowCount="5"
        app:npv_TextSizeNormal="16sp"
        app:npv_TextSizeSelected="20sp"
        app:npv_WrapSelectorWheel="true"/>

3.Java程式碼中使用:
1)若設定的資料(String[] mDisplayedValues)不會再次改變,可以使用如下方式進行設定:(與NumberPicker的設定方式一致)

        picker.setMinValue(minValue);
        picker.setMaxValue(maxValue);
        picker.setValue(value);

2)若設定的資料(String[] mDisplayedValues)會改變,可以使用如下組合方式進行設定:(與NumberPicker的更改資料方式一致)

        int minValue = getMinValue();
        int oldMaxValue = getMaxValue();
        int oldSpan = oldMaxValue - minValue + 1;
        int newMaxValue = display.length - 1;
        int newSpan = newMaxValue - minValue + 1;
        if (newSpan > oldSpan) {
            setDisplayedValues(display);
            setMaxValue(newMaxValue);
        } else {
            setMaxValue(newMaxValue);
            setDisplayedValues(display);
        }

或者直接使用NumberPickerView提供的方法:
refreshByNewDisplayedValues(String[] display)
使用此方法時需要注意保證資料改變前後的minValue值不變。

4.另外,NumberPickerView提供了平滑滾動的方法:
public void smoothScrollToValue(int fromValue, int toValue, boolean needRespond)

此方法與setValue(int)方法相同之處是可以動態設定當前顯示的item,不同之處在於此方法可以使NumberPickerView平滑的從滾動,即從fromValue值挑選最近路徑滾動到toValue,第三個引數needRespond用來標識在滑動過程中是否響應onValueChanged回撥函式。因為多個NumberPickerView在聯動時,很可能不同的NumberPickerView的停止時間不同,如果在此時響應了onValueChanged回撥,就可能再次聯動,造成資料不準確,將needRespond置為false,可避免在滑動中響應回撥函式。

另外,在使用此方法或者間接呼叫此方法時,需要注意最好不要在onCreate(Bundle savedInstanceState)方法中呼叫,因為scroll動畫需要一定時間,如需確要在onCreate(Bundle savedInstanceState)中呼叫,請使用如下方式:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //程式碼省略
        mNumberPickerView.post(new Runnable() {
            @Override
            public void run() {
                //呼叫smoothScrollToValue()等方法的程式碼
            }
        });
    }

5.各項自定義屬性的說明

    <declare-styleable name="NumberPickerView">
        <attr name="npv_ShowCount" format="reference|integer" />//顯示的條目個數,預設3個
        <attr name="npv_ShowDivider" format="reference|boolean" />//是否顯示兩條divider,預設顯示
        <attr name="npv_DividerColor" format="reference|color" />//兩條divider的顏色
        <attr name="npv_DividerMarginLeft" format="reference|dimension" />//divider距左側的距離
        <attr name="npv_DividerMarginRight" format="reference|dimension" />//divider距右側的距離
        <attr name="npv_DividerHeight" format="reference|dimension" />//divider的高度
        <attr name="npv_TextColorNormal" format="reference|color" />//未選中文字的顏色
        <attr name="npv_TextColorSelected" format="reference|color" />//選中文字的顏色
        <attr name="npv_TextColorHint" format="reference|color" />//中間偏右側說明文字的顏色
        <attr name="npv_TextSizeNormal" format="reference|dimension" />//未選中文字的大小
        <attr name="npv_TextSizeSelected" format="reference|dimension" />//選中文字的大小
        <attr name="npv_TextSizeHint" format="reference|dimension" />//說明文字的大小
        <attr name="npv_TextArray" format="reference" />//文字內容,stringarray型別
        <attr name="npv_MinValue" format="reference|integer" />//最小值,同setMinValue()
        <attr name="npv_MaxValue" format="reference|integer" />//最大值,同setMaxValue()
        <attr name="npv_WrapSelectorWheel" format="reference|boolean" />//設定是否wrap,同setWrapSelectorWheel
        <attr name="npv_HintText" format="reference|string" />//設定說明文字
        <attr name="npv_EmptyItemHint" format="reference|string" />//空行的顯示文字,預設不顯示任何文字。只在WrapSelectorWheel==false是起作用
        <attr name="npv_MarginStartOfHint" format="reference|dimension" />//說明文字距離左側的距離,"左側"是指文字array最寬item的右側
        <attr name="npv_MarginEndOfHint" format="reference|dimension" />//說明文字距離右側的距離
        <attr name="npv_ItemPaddingHorizental" format="reference|dimension" />//item的水平padding,用於wrap_content模式
        <attr name="npv_ItemPaddingVertical" format="reference|dimension" />//item的豎直padding,用於wrap_content模式

//以下屬性用於在wrap_content模式下,改變內容array並且又不想讓控制元件"跳動",那麼就可以設定所有改變的內容的最大寬度
        <!--just used to measure maxWidth for wrap_content without hint,
            the string array will never be displayed.
            you can set this attr if you want to keep the wraped numberpickerview
            width unchanged when alter the content list-->
        <attr name="npv_AlternativeTextArrayWithMeasureHint" format="reference" />//可能達到的最大寬度,包括說明文字在內,最大寬度只可能比此String的寬度更大
        <attr name="npv_AlternativeTextArrayWithoutMeasureHint" format="reference" />//可能達到的最大寬度,不包括說明文字在內,最大寬度只可能比此String的寬度+說明文字+說明文字marginstart +說明文字marginend 更大
        <!--the max length of hint content-->
        <attr name="npv_AlternativeHint" format="reference|string" />//說明文字的最大寬度
    </declare-styleable>

主要原理

一般情況下,我們只使用NumberPicker對文字進行選擇,很少涉及到新增不同的View甚至是圖片,因此,NumberPickerView只針對傳入的String[] 型別的內容通過onDraw(Canvas canvas)函式進行顯示。這裡主要涉及三個知識點:

1.滾動效果的產生:

Android的framework中帶有一個工具類Scroller,此類的功能是根據輸入值以及當前持續的時間,配合插值計算器,計算出當前的輸出值。輸入值一般是速度和座標值,輸出值是座標值,舉例來說,可以將其想像成高中物理根據初始速度、加速度和執行時間求取當前路程。Scroller的實現比較巧妙,它不會在startScroll()後去計算當前“路程”,而是隻記錄“出發”時的各種狀態,只有當外部需要知道此時滑動到的位置時,才會去根據時間以及插值來進行計算,這種設計避免了另起執行緒實時計算等問題。
那麼問題來了,什麼時候去獲取scroller中當前的座標值呢?閱讀View類的原始碼,我們可以看到在View的boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)函式中,有如下程式碼:

if (!drawingWithRenderNode) {
    computeScroll();
    sx = mScrollX;
    sy = mScrollY;
}

即在繪製view時,先呼叫computeScroll(),因此我們可以在computeScroll()函式重新設定或重新整理當前的座標,並在onDraw(Canvas canvas)函式中根據當前的狀態重新繪製此view。

computeScroll()的使用方法通常為:

@Override
public void computeScroll() {
     //如果Scroller仍然在滾動
    if (mScroller.computeScrollOffset()) {
       //獲取mScroller中的值並計算和記錄,如 mGlobalY = mScroller.getCurrY();
       //重新整理
        postInvalidate();
    }
}

在NumberPickerView中,我使用一個globalY值來記錄當前滾動到的座標,並通過與Item的高度相除,計算當前應該顯示的item的偏移值,從而畫出當前只在顯示區域的item。

繪製的思路明確了,再來看滑動跟隨手指移動以及fling效果。這兩者均是通過override onTouchEvent(MotionEvent event)函式來實現的,不過實現的具體過程稍有不同:
(1). 跟隨手指滑動是複寫了MotionEvent.ACTION_MOVE的情況,根據當前手指移動的座標值,計算globalY值,並重新整理顯示。
(2). fling效果是通過VelocityTracker工具類實現的,此工具類可以根據接收的兩個連續發出的MotionEvent來獲取當前的滑動速度,若速度大於閾值,則將初始座標值、VelocityTracker計算獲取的速度傳遞給Scroller.fling(...)函式,此時再次重新整理,即可通過computeScroll()函式,獲取當前globalY值,進而更新繪圖。

2.自動校準位置。

為了實現自動校準,我在每次手指Up或者Scroller開始Fling時,均通過handler傳送refresh訊息,在接受到此訊息時,計算當前位置是否處於被校準的位置,如果否,則計算需要滑動的位移,並將值傳遞給Scroller,進而進行校準滑動。當確保已經校準後,暫停傳送refresh訊息。

3.漸變的UI效果

漸變UI效果同樣是通過計算當前滑動的座標以及某個item與中間顯示位置的差值比例,來確定此item中的字型大小以及顏色。

將NumberPicker改為NumberPickerView

要替代專案中使用的NumberPicker,只需要將涉及NumberPicker的程式碼(如回撥中傳入了NumberPicker、使用了NumberPicker的內部介面)改為NumberPickerView即可。

天若有情天亦老,Star一下好不好?

歡迎大家不吝指教。
email: [email protected]