如果你看到了這篇文章,希望你耐心的把它看完。我保證你認真看完了全部,一定不會讓你失望。因為你可能遇到了史上最好用的PickerView庫。
本文會通過和目前github上最受歡迎的PickerView庫做詳細的對比,通過對比你可以清晰的感受它的好用和強大。
Github地址:https://github.com/jaaksi/pickerview
一個非常好用的Android PickerView庫,內部提供3種常用類型的Picker。支持擴展自定義Picker。
- TimePicker:時間選擇器,包含日期
- MixedTimePicker::聚合的時間選擇器
- OptionPicker:聯動選擇器
Screenshot
APK
Demo App下載連接
PickerView
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