這可能是史上最好用的PickerView庫了

如果你看到了這篇文章,希望你耐心的把它看完。我保證你認真看完了全部,一定不會讓你失望。因為你可能遇到了史上最好用的PickerView庫。
本文會通過和目前github上最受歡迎的PickerView庫做詳細的對比,通過對比你可以清晰的感受它的好用和強大。

Github地址:https://github.com/jaaksi/pickerview

一個非常好用的Android PickerView庫,內部提供3種常用類型的Picker。支持擴展自定義Picker。

  • TimePicker:時間選擇器,包含日期
  • MixedTimePicker::聚合的時間選擇器
  • OptionPicker:聯動選擇器

Screenshot

TimePicker.gif
MixedTimePicker.gif
custom.png

APK

Demo App下載連接

PickerView

README

Picker

通過組裝PickerView實現常用的Picker選擇器。上面已經列舉提供的3中常用的Picker。

BasePicker

Picker基類:封裝了TopBar,PickerView容器,create and add PickerView方法,Picker彈窗等方法。
三種Picker都繼承自BasePicker,你也可以繼承它擴展自己的Picker。

API

api description
setPickerBackgroundColor 設置picker背景
setPadding 設置PickerView父容器padding 單位:px
setTag 給Picker 設置tag,用于區分不同的picker等。用法同View setTag
getRootLayout 獲取PickerView的父容器,創建DefaultTopBar時必須指定
setOnPickerChooseListener 設置picker取消,確定按鈕監聽。可用于攔截選中操作
setTopBar 設置自定義TopBar
setInterceptor 設置攔截器
createPickerView 創建PickerView
getPickerViews 獲取Picker中所有的pickerview集合
addPicker 將創建的PickerView 添加到上面集合中,createPickerView內部已調用該方法
findPickerViewByTag 通過tag找到對應的PickerView
isScrolling 是否滾動未停止。滾動未停止的時候,不響應Picker的取消,確定按鍵
getPickerDialog 獲取Picker彈窗。可以在new之后設置dialog屬性
show 顯示picker彈窗

對比github上最受歡迎的同類庫 Android-PickerView
本庫將TopBar等通用相關邏輯封裝在基類中,并提供代碼中創建PickerView方法,不需要再依賴xml。用戶自定義Picker時,繼承BasePicker,只需要處理自己的邏輯即可,簡單便捷。
而對Android-PickerView來說,實現自定義Picker,依然需要處理TopBar等邏輯。造成大量重復代碼。

TopBar

TopBar:TopBar通過抽象接口ITopBar來管理,實現Picker與TopBar的解耦。提供默認實現DefaultTopBar。可實現接口定制自己的TopBar。

   public interface ITopBar {
     /**
      * @return topbar view
      */
     View getTopBarView();

     /**
      * @return 取消按鈕view
      */
     View getBtnCancel();

     /**
      * @return 確定按鈕view
      */
     View getBtnConfirm();

     /**
      * @return title view
      */
     TextView getTitleView();
   }

DefaultTopBar API

api description
setDividerColor 設置topbar bottom line color
setDividerHeight 設置bottom divider line height
getDivider 獲取TopBar bottom line
getTitleView 獲取TopBar title view

Interceptor

攔截器:用于在pickerview創建時攔截,設置pickerview的屬性。

Picker內部并不提供對PickerView的設置方法,而是通過Interceptor實現。這種設計用來實現Picker和PickerView的屬性設置完美解耦。

   private void init(){
    mTimePicker.setInterceptor(new BasePicker.Interceptor() {
      @Override public void intercept(PickerView pickerView) {
        pickerView.setVisibleItemCount(5);
        // 將年月設置為循環的
        int type = (int) pickerView.getTag();
        if (type == TimePicker.TYPE_YEAR || type == TimePicker.TYPE_MONTH) {
          pickerView.setIsCirculation(true);
        }
      }
    })
   }

這一點對比 Android-PickerView, 每個Picker都需要聲明對PickerView的設置方法,與PickerView嚴重耦合。需要開發者copy大量重復代碼,且無法區分每一個PickerView設置不同的屬性。

TimePicker

常用的時間選擇器,支持 年、月、日、時、分

  • 時間類型type的設計:自由組合、隨心所欲(當然應該是有意義的)
  TYPE_YEAR | TYPE_MONTH | TYPE_DAY | TYPE_HOUR | TYPE_MINUTE

對比 Android-PickerView TimePickerView

  /**
  * Android-PickerView中的設置type方法:參數設置麻煩且不易理解
  * 長度必須為6的數組,表示年月日時分秒 的顯示與否,不設置則默認全部顯示
  */
  setType(boolean[] type)
  
  // 本項目設置type方法:簡單易懂,組合方便
  setType(TYPE_DATE | TYPE_HOUR)
  • 完美支持時間區間設置以及選中聯動
  • 支持Format,如顯示今年,明年

API

api description
type 時間類型,需要在Builder構造方法中指定,不能改變
OnTimeSelectListener 選中時間回調,需要在Builder構造方法中指定,不能改變
setRangDate 設置起止時間
setSelectedDate 設置選中時間戳
setInterceptor 設置攔截器
setFormatter 設置Formatter,內部提供默認的Formatter
create 通過Builder構建 TimePicker
以上是TimePicker.Builder的,下面是TimePicker的
setFormatter 同上
setSelectedDate 同上
getType 獲取type
hasType 判斷是否包含某種type

Formatter

TimePicker Formatter:用于根據type和num格式化時間文案

  public interface Formatter {
    /**
     * 根據type和num格式化時間
     *
     * @param picker picker
     * @param type 并不是模式,而是當前item所屬的type,如年,時
     * @param position position
     * @param num position item顯示的數字
     */
    CharSequence format(TimePicker picker, int type, int position, int num);
  }

內部提供默認的 Formatter實現DefaultFormatter。用戶可以設置自定義Formatter或繼承DefaultFormatter進行擴展。

TimePicker初始化,如果未設置時間區間,會使用默認區間。三種Picker都采用Builder模式初始化。且用戶自定義的Picker也應該采用這種模式進行初始化。

Simple Example

    mTimePicker = new TimePicker.Builder(mActivity, type, this)
      // 設置時間區間
      .setRangDate(1526361240000L, 1893563460000L)
      // 設置選中時間
      //.setSelectedDate()
      // 設置pickerview樣式
      .setInterceptor(new BasePicker.Interceptor() {
        @Override public void intercept(PickerView pickerView) {
          pickerView.setVisibleItemCount(5);
          // 將年月設置為循環的
          int type = (int) pickerView.getTag();
          if (type == TimePicker.TYPE_YEAR || type == TimePicker.TYPE_MONTH) {
            pickerView.setIsCirculation(true);
          }
        }
      })
      // 設置 Formatter
      .setFormatter(new TimePicker.DefaultFormatter() {
        // 自定義Formatter顯示去年,今年,明年
        @Override public CharSequence format(TimePicker picker, int type, int position, int num) {
          if (type == TimePicker.TYPE_YEAR) {
            int offset = num - mCurrYear;
            if (offset == -1) return "去年";
            if (offset == 0) return "今年";
            if (offset == 1) return "明年";
            return num + "年";
          }

          return super.format(picker, type, position, num);
        }
      }).create();

    //mTimePicker.setSelectedDate(1549349843000L);
    mTimePicker.show();

MixedTimePicker

常用的聚合時間選擇器。日期(年、月、日)聚合,時間(小時、分鐘)聚合。

  • 混合模式:github上的TimePicker庫基本都不提供該種類型的Picker
  • 支持自定義日期格式,時間格式
  • 支持設置時間間隔
  • 支持設置區間以及選中聯動
  • 支持設置純日期,純時間模式,采用type同TimePicker

API

api description
type 類型,需要在Builder構造方法中指定,不能改變
OnTimeSelectListener 選中時間回調,需要在Builder構造方法中指定,不能改變
setRangDate 設置起止時間
setSelectedDate 設置選中時間戳
setTimeMinuteOffset 設置時間間隔分鐘數(60%offset==0才有效),以0為起始邊界
setContainsStarDate 設置mTimeMinuteOffset作用時,是否包含超出的startDate
setContainsEndDate 設置mTimeMinuteOffset作用時,是否包含超出的endDate
setInterceptor 設置攔截器
setFormatter 設置Formatter,內部提供默認的Formatter
create 通過Builder構建 MixedTimePicker
以上是MixedTimePicker.Builder的,下面是MixedTimePicker的
setFormatter 同上
setSelectedDate 同上
getType 獲取type
hasType 判斷是否包含某種type

Formatter

MixedTimePicker Formatter:用于自定義日期和時間格式。內部提供默認的 Formatter實現。

  public interface Formatter {
    /**
     * 用戶可以自定義日期格式和時間格式
     *
     * @param picker picker
     * @param date 當前狀態對應的日期或者時間
     * @param position 當前type所在的position
     */
    CharSequence format(MixedTimePicker picker, int type, Date date, int position);
    }

MixedTimePicker 的 Formatter 完美體現了Formatter設計的精妙之處。用戶可以根據回調中的type和date自定義日期和時間格式。比如顯示今天,或 xx月xx日 星期 x

Simple Example

    mTimePicker = new MixedTimePicker.Builder(mActivity, MixedTimePicker.TYPE_ALL, this)
      // 設置不包含超出的結束時間<=
      .setContainsEndDate(false)
      // 設置時間間隔為30分鐘
      .setTimeMinuteOffset(30)
      .setRangDate(1517771651000L, 1577976666000L)
      .setFormatter(new MixedTimePicker.DefaultFormatter() {
        @Override
        public CharSequence format(MixedTimePicker picker, int type, Date date, int position) {
          if (type == MixedTimePicker.TYPE_DATE) {
            CharSequence text;
            int dayOffset = DateUtil.getDayOffset(date.getTime(), System.currentTimeMillis());
            if (dayOffset == 0) {
              text = "今天";
            } else if (dayOffset == 1) {
              text = "明天";
            } else { // xx月xx日 星期 x
              text = mDateFormat.format(date);
            }
            return text;
          }
          return super.format(picker, type, date, position);
        }
      })
      .create();
    // 2018/2/5 03:14:11 - 2020/1/2 22:51:6
    Dialog pickerDialog = mTimePicker.getPickerDialog();
    pickerDialog.setCanceledOnTouchOutside(true);
    DefaultTopBar topBar = (DefaultTopBar) mTimePicker.getTopBar();
    topBar.getTitleView().setText("請選擇時間");

不同于TimePicker, MixedTimePicker 由于支持純時間模式(日期取選中時間的日期),<font color=red>不提供默認區間。如果模式中包含日期模式,則會強制要求設置時間區間</font>

OptionPicker

  • 支持設置層級
  • 構造數據源及其簡單,只需要實現OptionDataSet接口
  • 支持通過對應選中的values設置選中項。內部處理選中項邏輯,避免用戶記錄下標且麻煩的遍歷處理

對比 Android-PickerView的 OptionsPickerView

function Android-PickerViews 本控件
多級 最多支持3級(寫死的) 構造時設置級別(無限制)
構造數據源 需要構建每一級的集合,二三級為嵌套 一級數據entity實現OptionDataSet接口即可
設置數據源 提供三個方法,分別用于一、二、三級的 只需要設置一級數據集
聯動選中 提供三個,只能設置選中的下標。
需要用戶自己通過多層遍歷定位每一級別選中的下標,然后再設置
只需要傳入選中的values(可變長數組),不需要任何計算

Android-PickerView 中的 OptionsPickerView 代碼。由于不知道層級,所以每個方法都提供3個用來對應(最多)3級選擇。

    // 提供3個選中的方法,分別對應1,2,3級聯動的情況
    public void setSelectOptions(int option1)

    public void setSelectOptions(int option1, int option2)

    public void setSelectOptions(int option1, int option2, int option3)

    // 提供3個設置數據源的方法,分別對應1,2,3級聯動的情況
    public void setPicker(List<T> optionsItems)

    public void setPicker(List<T> options1Items, List<List<T>> options2Items)

    public void setPicker(List<T> options1Items, List<List<T>> options2Items,
          List<List<List<T>>> options3Items) {
    }

本庫中的OptionPicker

/**
   * 根據選中的values初始化選中的position并初始化pickerview數據
   *
   * @param options data
   * @param values 選中數據的value{@link OptionDataSet#getValue()}
   */
  public void setDataWithValues(List<? extends OptionDataSet> options, String... values) {
    mOptions = options;
    setSelectedWithValues(values);
  }

  /**
   * 根據選中的values初始化選中的position
   *
   * @param values 選中數據的value{@link OptionDataSet#getValue()},如果values[0]==null,則進行默認選中,其他為null認為沒有該列
   */
  public void setSelectedWithValues(String... values) {
  ...
  }

如上面對比表格中所列舉的,無論是層級,構造數據源和設置數據源,還是設置選中的選項,本庫的API都十分簡單,方便。

API

api description
mHierarchy 層級,需要在Builder構造方法中指定,不能改變
OnOptionSelectListener 選中回調,需要在Builder構造方法中指定,不能改變
setInterceptor 設置攔截器
setFormatter 設置Formatter
create 通過Builder構建 OptionPicker
以上是OptionPicker.Builder的,下面是OptionPicker的
setFormatter 同上
setDataWithValues 根據選中的values初始化選中的position并初始化pickerview數據。
values參數為可變長數組,可以不設置。
setDataWithIndexs 設置數據和選中position。不建議使用,建議使用 setDataWithValues
setSelectedWithValues 根據選中的values初始化選中的position
setSelectedWithIndexs 設置選中的position。不建議使用,建議使用 setSelectedWithValues
getOptions 獲取數據集
getSelectedPosition 獲取選中的下標,數組size=mHierarchy,如果為-1表示該列沒有數據
getSelectedOptions 獲取選中的選項,如果指定index為null則表示該列沒有數據

需要注意的是:本庫中的OptionPicker只用于聯動的,不支持多級別且不聯動。
基本沒有這種需求,如果大家有這種需求,我會在后續迭代中支持。

Others

奇葩設計:部分default屬性聲明為static而非final

全局設置default屬性

奇葩也好,亮點也罷。作為一個UI控件,不同的app,不同的UI,不同的產品自然會有不同的樣式。
考慮到在一個app中我們會用到很多Picker,而我們又需要定制自己的UI的樣式,如果通過動態方法設置樣式就太麻煩了。
故做此設計。你可以通過配置這些static變量來快速定制一個滿足自己app樣式需求的Picker。
當然你也可以通過封裝方法來處理PickerView,Picker,裝飾器等樣式,但這樣一樣十分麻煩。我相信你自己都會煩。

靜態默認值

所有的這些靜態屬性值都以 sDefault 開頭

  • BasePickerView
field description defaultValue
sDefaultVisibleItemCount 默認可見的item個數 5
sDefaultItemSize 默認itemSize 50(dp)
sDefaultIsCirculation 默認是否循環 false
  • PickerView
field description defaultValue
sOutTextSize default out text size 18(dp)
sCenterTextSize default center text size 22(dp)
sCenterColor default center text color Color.BLUE
sOutColor default out text color Color.GRAY
  • BasePicker
field description defaultValue
sDefaultPaddingRect pickerView父容器的 default padding null(無padding)
sDefaultPickerBackgroundColor default picker background color Color.WHITE
sDefaultTopBarCreator 用于構建自定義defaultTopBar的接口 null
  • DefaultCenterDecoration
field description defaultValue
sDefaultLineColor default line color Color.BLUE
sDefaultLineWidth default line width 1(dp)
sDefaultDrawable default item background drawable null
sDefaultMarginRect default line margin null(無margin)

建議初始化這些屬性值放到Application中完成,避免app發生crash而導致失效

Simple Example

public class MyApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    // 建議在application中初始化picker 默認屬性實現全局設置
    initDefaultPicker();
  }

  private void initDefaultPicker() {
    // 利用修改靜態默認屬性值,快速定制一套滿足自己app樣式需求的Picker.
    // BasePickerView
    PickerView.sDefaultVisibleItemCount = 3;
    PickerView.sDefaultItemSize = 50;
    PickerView.sDefaultIsCirculation = true;

    // PickerView
    PickerView.sOutTextSize = 18;
    PickerView.sCenterTextSize = 18;
    PickerView.sCenterColor = Color.RED;
    PickerView.sOutColor = Color.GRAY;

    // BasePicker
    int padding = Util.dip2px(this, 20);
    BasePicker.sDefaultPaddingRect = new Rect(padding, padding, padding, padding);
    BasePicker.sDefaultPickerBackgroundColor = Color.WHITE;
    // 自定義 TopBar
    BasePicker.sDefaultTopBarCreator = new BasePicker.IDefaultTopBarCreator() {
      @Override public ITopBar createDefaultTopBar(LinearLayout parent) {
        return new CustomTopBar(parent);
      }
    };

    // DefaultCenterDecoration
    DefaultCenterDecoration.sDefaultLineWidth = 1;
    DefaultCenterDecoration.sDefaultLineColor = Color.RED;
    //DefaultCenterDecoration.sDefaultDrawable = new ColorDrawable(Color.WHITE);
    int leftMargin = Util.dip2px(this, 10);
    int topMargin = Util.dip2px(this, 2);
    DefaultCenterDecoration.sDefaultMarginRect =
      new Rect(leftMargin, -topMargin, leftMargin, -topMargin);
  }
}

Change Log

v1.0.0(2018-03-03)

  • release v1.0.0

Gradle

compile 'org.jaaksi:pickerview:1.0.0'

Thanks

感謝你耐心的看完。歡迎大家提意見。
Github地址:https://github.com/jaaksi/pickerview

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,288評論 25 708
  • 內容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,886評論 22 665
  • 緣于從小就對文字有種偏愛的緣故吧,沒有刻意的去找尋,偶遇簡書,不了解有關簡書的一切,僅"簡書‘’一詞便讓我留戀駐足...
    蟄伏賢居閱讀 222評論 0 1
  • 三小姐:怎么想到推薦《圣經的故事》這本書? 六先生:有兩方面的原因,情感的和理性的。 三小姐:情感的原因,這怎么講...
    三六書屋閱讀 513評論 2 1