DatePicker的缺陷

  1. 提供的API太少,沒辦法個性化定製。比如,不能指定某部分的顏色,不能控制顯示的部分等。
  2. xml中提供的屬性太少,同樣影響定製化。
  3. 相容性問題太多,在4.x,5.x和6.0+上UI不同
  4. 同樣是相容性問題,同一個屬性設定在不同的系統版本上有不同的效果
  5. bug太多,暫且發現下面這6個
    • bug1:日曆模式,在5.0以下設定的可選時間區間如果與當前日期在同一欄會crash
    • bug2:LOLLIPOP上OnDateChangedListener回撥無效(5.0上存在,5.1修復)
    • bug3:5.0上超過可選區間的日期依然能選中,所以要手動校驗.5.1上已解決.
    • bug4:LOLLIPOP和Marshmallow上,使用spinner模式,然後隱藏滾輪,顯示日曆,日曆最底部一排日期被截去部分
    • bug5:5.1上,maxdate不可選。由於5.0有bug3,所以可能bug5被掩蓋了。4.x和6.0+版本沒有這個問題。
    • bug6:bug5在6.0+上有另一個表現形式,currentDate如果與MaxDate一樣,初始化時會觸發一個onDateChanged回撥。其內部原因都是一樣的。

DatePicker的使用

由於Google在Android4.x上採用的是holo風格,在5.x及以上採用的Material Design風格,所以從4.x到5.x,Google重構了DatePicker,包括程式碼和UI。所以這就產生了相容性問題。

我要解決的問題

  1. UI的一致性。

    • 在4.x上,DatePicker沒有Mode的概念,預設就是滾輪和日曆並排顯示,但可通過xml或者程式碼,控制只顯示滾輪或者只顯示日曆。但是日曆模式下,存在上述bug1的問題,所以與老大商量了一下,考慮到4.x系統佔比太小,可以使用滾輪模式。

       
      DatePicker在4.x
    • 在5.x及以上,DatePicker引入了Mode的概念,spinner和calendar只能顯示其中一個,所以可以在xml直接指定calendar模式。但是5.x和6.0+的日曆都多了一個頭部,而且5.x和6.0+的頭部還不一樣,又沒有API可以隱藏頭部。所以,需要自己想辦法隱藏頭部。
 
DatePicker在5.x
 
DatePicker在6.0+
  1. 定製DatePicker,符合射雞師的要求。
    DatePicker的能用來做個性化的API和屬性值太少了,正常途徑我要改變選中日期的圓圈顏色都做到。其實,系統提供的控制元件多半是從系統提供的style中讀取配置,我們可以自己配置一個style給DatePicker。
    如果在Activity中使用DatePicker,DatePicker會讀取Activity的Theme;如果在Dialog中使用DatePicker,會讀取Dialog的Theme(如果Dialog沒有指定Theme,預設使用Activity的Theme)。我們要在Dialog中使用DatePicker,所以自定義一個DatePicker的style,傳給自定義的Dialog的Theme,再使用自定義的Theme建立Dialog就好了。
    其實系統提供了幾個預設Theme,通過它們可以簡單改變DatePicker的風格,參考這個答案。但其實這些Theme內部也是通過改變DatePicker(通過datepickerstyle)的屬性來做到的。

  2. 解決上述發現的bug。
    要解決相容性問題,也要解決bug,所以在程式碼中必須分情況處理。

程式碼

程式碼量很少,註釋也寫的很清楚,相信看完就懂了。

  1. 內部封裝DatePicker的DialogFragment
public class CustomDatePickerDialogFragment extends DialogFragment  implements DatePicker.OnDateChangedListener, View.OnClickListener{
public static final String CURRENT_DATE = "datepicker_current_date";
public static final String START_DATE = "datepicker_start_date";
public static final String END_DATE = "datepicker_end_date";
Calendar currentDate;
Calendar startDate;
Calendar endDate; DatePicker datePicker;
TextView backButton;
TextView ensureButton;
View splitLineV; @Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setCancelable(false);
Bundle bundle = getArguments();
currentDate = (Calendar) bundle.getSerializable(CURRENT_DATE);
startDate = (Calendar) bundle.getSerializable(START_DATE);
endDate = (Calendar) bundle.getSerializable(END_DATE);
} @Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (inflater == null) {
return super.onCreateView(inflater, container, savedInstanceState);
}
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
getDialog().getWindow().setDimAmount(0.8f);
View view = inflater.inflate(R.layout.dialog_date_picker_layout,container,false);
return view;
} @NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
int style;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
style = R.style.ZZBDatePickerDialogLStyle;
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
style = R.style.ZZBDatePickerDialogLStyle;
} else {
style = getTheme();
}
return new Dialog(getActivity(), style);
} @Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (view != null) {
datePicker = view.findViewById(R.id.datePickerView);
backButton = view.findViewById(R.id.back);
backButton.setOnClickListener(this);
ensureButton = view.findViewById(R.id.ensure);
ensureButton.setOnClickListener(this);
splitLineV = view.findViewById(R.id.splitLineV);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
//bug1:日曆模式,在5.0以下設定的可選時間區間如果與當前日期在同一欄會crash,所以只能用滾輪模式
datePicker.setCalendarViewShown(false);
datePicker.setSpinnersShown(true);
//滾輪模式必須使用確定選單
ensureButton.setVisibility(View.VISIBLE);
splitLineV.setVisibility(View.VISIBLE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
//bug2:LOLLIPOP上OnDateChangedListener回撥無效(5.0存在,5.1修復),必須使用確定選單回傳選定日期
ensureButton.setVisibility(View.VISIBLE);
splitLineV.setVisibility(View.VISIBLE);
//如果只要日曆部分,隱藏header
ViewGroup mContainer = (ViewGroup) datePicker.getChildAt();
View header = mContainer.getChildAt();
header.setVisibility(View.GONE);
} else {
//bug4:LOLLIPOP和Marshmallow上,使用spinner模式,然後隱藏滾輪,顯示日曆(spinner模式下的日曆沒有頭部),日曆最底部一排日期被截去部分。所以只能使用calender模式,然後手動隱藏header(系統沒有提供隱藏header的api)。
//如果只要日曆部分,隱藏header
ViewGroup mContainer = (ViewGroup) datePicker.getChildAt();
View header = mContainer.getChildAt();
header.setVisibility(View.GONE);
//Marshmallow上底部留白太多,減小間距
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) datePicker.getLayoutParams();
layoutParams.bottomMargin = ;
datePicker.setLayoutParams(layoutParams);
}
initDatePicker();
}
} private void initDatePicker() {
if (datePicker == null) {
return;
}
if (currentDate == null) {
currentDate = Calendar.getInstance();
currentDate.setTimeInMillis(System.currentTimeMillis());
}
datePicker.init(currentDate.get(Calendar.YEAR),currentDate.get(Calendar.MONTH),currentDate.get(Calendar.DAY_OF_MONTH),this);
if (startDate != null) {
datePicker.setMinDate(startDate.getTimeInMillis());
}
if (endDate != null) {
//bug5:5.1上,maxdate不可選。由於5.0有bug3,所以可能bug5被掩蓋了。4.x和6.0+版本沒有這個問題。
//bug5在6.0+上有另一個表現形式:初始化時會觸發一次onDateChanged回撥。通過原始碼分析一下原因:init方法只會設定控制元件當前日期的
//年月日,而時分秒預設使用現在時間的時分秒,所以當前日期大於>最大日期,執行setMaxDate方法時,就會觸發一次onDateChanged回撥。
//同理,setMinDate方法也面臨同樣的方法。所以設定範圍時,MinDate取0時0分0秒,MaxDate取23時59分59秒。
endDate.set(Calendar.HOUR_OF_DAY,);
endDate.set(Calendar.MINUTE,);
endDate.set(Calendar.SECOND,);
datePicker.setMaxDate(endDate.getTimeInMillis());
}
} @Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.back:
dismiss();
break;
case R.id.ensure:
returnSelectedDateUnderLOLLIPOP();
break;
default:
break;
}
} private void returnSelectedDateUnderLOLLIPOP() {
//bug3:5.0上超過可選區間的日期依然能選中,所以要手動校驗.5.1上已解決,但是為了與5.0保持一致,也採用確定選單返回日期
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
Calendar selectedDate = Calendar.getInstance();
selectedDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth(),,,);
selectedDate.set(Calendar.MILLISECOND,);
if (selectedDate.before(startDate) || selectedDate.after(endDate)) {
Toast.makeText(getActivity(), "日期超出有效範圍", Toast.LENGTH_SHORT).show();
return;
}
}
if (onSelectedDateListener != null) {
onSelectedDateListener.onSelectedDate(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth());
}
dismiss();
} @Override
public void onDestroyView() {
super.onDestroyView();
onSelectedDateListener = null;
} public interface OnSelectedDateListener {
void onSelectedDate(int year, int monthOfYear, int dayOfMonth);
} OnSelectedDateListener onSelectedDateListener; public void setOnSelectedDateListener(OnSelectedDateListener onSelectedDateListener) {
this.onSelectedDateListener = onSelectedDateListener;
} @Override
public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.M){ //LOLLIPOP上,這個回撥無效,排除將來可能的干擾
return;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { //5.0以下,必須採用滾輪模式,所以需藉助確定選單回傳選定值
return;
}
if (onSelectedDateListener != null) {
onSelectedDateListener.onSelectedDate(year, monthOfYear, dayOfMonth);
}
dismiss();
}
}
  1. CustomDatePickerDialogFragment的layout檔案 - R.layout.dialog_date_picker_layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"> <DatePicker
android:id="@+id/datePickerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnersShown="false"
android:calendarViewShown="true"
android:datePickerMode="calendar"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"/> <View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/black" /> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"> <TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=""
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="返回"
android:gravity="center"
android:textColor="@android:color/black"
android:id="@+id/back"/> <View
android:layout_width="1px"
android:layout_height="match_parent"
android:background="@android:color/black"
android:id="@+id/splitLineV"
android:visibility="gone"/> <TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=""
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="確認"
android:gravity="center"
android:textColor="@android:color/black"
android:id="@+id/ensure"
android:visibility="gone"/> </LinearLayout> </LinearLayout>
  1. 自定義的DatePicker的style
<style name="ZZBDatePickerDialogLStyle" parent="android:Theme.DeviceDefault.Light.Dialog">
<item name="android:datePickerStyle">@style/ZZBDatePickerLStyle</item>
<!-- 初始化的那一天和選中時的圓圈的顏色-->
<item name="android:colorControlActivated">@android:color/holo_blue_dark</item>
<!-- LOLLIPOP,整個日曆字型的顏色。Marshmallow,日曆中星期字型顏色-->
<item name="android:textColorSecondary">@android:color/holo_blue_dark</item>
<!-- Marshmallow,日曆字型的顏色,不可選的日期依然有置灰效果。LOLLIPOP,無效-->
<item name="android:textColorPrimary">@android:color/holo_purple</item>
</style> <style name="ZZBDatePickerLStyle" parent="android:Widget.Material.Light.DatePicker">
<!-- LOLLIPOP,最頂部,星期標題的背景色。Marshmallow星期標題被合併到header,所以欄位無效-->
<item name="android:dayOfWeekBackground">@android:color/holo_blue_light</item>
<!-- LOLLIPOP,最頂部,星期字型的顏色、大小等。Marshmallow星期標題被合併到header,所以欄位無效-->
<item name="android:dayOfWeekTextAppearance">@style/ZZBTitleDayOfWeekTextAppearance</item>
<!-- 中間部分,header的背景色 -->
<item name="android:headerBackground" >@android:color/holo_orange_dark</item>
<!-- 中間部分,header的字型大小和顏色-->
<!-- 對LOLLIPOP有效,對Marshmallow無效-->
<item name="android:headerYearTextAppearance">@style/ZZBHeaderYearTextAppearance</item>
<!-- 對LOLLIPOP和Marshmallow都是部分有效-->
<item name="android:headerMonthTextAppearance">@style/ZZBHeaderMonthTextAppearance</item>
<!-- 對LOLLIPOP有效,對Marshmallow無效-->
<item name="android:headerDayOfMonthTextAppearance">@style/ZZBHeaderDayOfMonthTextAppearance</item>
<!-- LOLLIPOP,控制整個日曆字型顏色的最終欄位,優先順序最高,但是一旦使用了這個欄位,不可選的日期就失去了置灰效果。對Marshmallow無效-->
<item name="android:calendarTextColor">@android:color/holo_green_dark</item>
</style> <style name="ZZBTitleDayOfWeekTextAppearance" parent="android:TextAppearance.Material">
<item name="android:textColor">@android:color/black</item>
<item name="android:textSize">12sp</item>
</style>
<style name="ZZBHeaderYearTextAppearance" parent="android:TextAppearance.Material">
<item name="android:textColor">@android:color/holo_blue_light</item>
<item name="android:textSize">50sp</item>
</style>
<style name="ZZBHeaderMonthTextAppearance" parent="android:TextAppearance.Material">
<!-- LOLLIPOP無效,Marshmallow有效。控制Marshmallow中header部分所有的字型顏色。LOLLIPOP沒有找到控制字型顏色的欄位-->
<item name="android:textColor">@android:color/holo_blue_light</item>
<!-- LOLLIPOP有效,Marshmallow無效。Marshmallow沒有找到控制header字型大小的欄位-->
<item name="android:textSize">50sp</item>
</style>
<style name="ZZBHeaderDayOfMonthTextAppearance" parent="android:TextAppearance.Material">
<!-- 只可以控制字型的大小,沒有找到控制字型顏色的欄位-->
<item name="android:textSize">50sp</item>
</style>
  1. MainActivity的程式碼
public class MainActivity extends AppCompatActivity implements View.OnClickListener,CustomDatePickerDialogFragment.OnSelectedDateListener{
Button button; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.datepicker);
button.setOnClickListener(this);
} @Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.datepicker:
showDatePickDialog();
break;
default:
break;
}
} long day = * * * ; private void showDatePickDialog() {
CustomDatePickerDialogFragment fragment = new CustomDatePickerDialogFragment();
fragment.setOnSelectedDateListener(this);
Bundle bundle = new Bundle();
Calendar currentDate = Calendar.getInstance();
currentDate.setTimeInMillis(System.currentTimeMillis());
currentDate.set(Calendar.HOUR_OF_DAY,);
currentDate.set(Calendar.MINUTE,);
currentDate.set(Calendar.SECOND,);
currentDate.set(Calendar.MILLISECOND,);
bundle.putSerializable(CustomDatePickerDialogFragment.CURRENT_DATE,currentDate); long start = currentDate.getTimeInMillis() - day * ;
long end = currentDate.getTimeInMillis() - day;
Calendar startDate = Calendar.getInstance();
startDate.setTimeInMillis(start);
Calendar endDate = Calendar.getInstance();
endDate.setTimeInMillis(end);
bundle.putSerializable(CustomDatePickerDialogFragment.START_DATE,startDate);
bundle.putSerializable(CustomDatePickerDialogFragment.END_DATE,currentDate); fragment.setArguments(bundle);
fragment.show(getSupportFragmentManager(),CustomDatePickerDialogFragment.class.getSimpleName());
} @Override
public void onSelectedDate(int year, int monthOfYear, int dayOfMonth) {
Toast.makeText(MainActivity.this,year+"年"+(monthOfYear+)+"月"+dayOfMonth+"日",Toast.LENGTH_SHORT).show();
}
  1. MainActivity的Layout檔案
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp" > <Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="DatePickerDialog"
android:id="@+id/datepicker"/> </LinearLayout>

  

效果截圖

  1. Jelly Bean
 
DatePicker效果在4.x
  1. Lollipop
 
DatePicker效果在5.x
  1. Marshmallow
 
DatePicker效果在6.0
  1. 最後再貼一張符合設計稿的效果圖
    我隱藏了頭部,並且把ZZBDatePickerDialogLStyle中的顏色值都改成了設計稿中的顏色。
 
datepickerfordesign在6.0+
 
datepickerfordesign在5.x
 
datepickerfordesign在4.x

可見在6.0+上效果最好。

最後

除了自己用DialogFragment封裝,系統還直接給提供了DatePickerDialog,可以直接以對話方塊的形式使用,但是這樣就不夠靈活了。

作者:華枯榮
轉自:https://www.jianshu.com/p/6700e0422e6e

參考文章

  1. Change Datepicker dialog color for Android 5.0
  2. 【Android開源庫合集】日曆效果 - 如果需要更強大的效果,還是第三方開源庫靠譜
  3. 修改DatePicker、 NumberPicker 預設屬性(間距、分割線顏色和高度) - 這篇文章提出了用反射和getIdentifier方法獲取並修改隱藏屬性,很有啟發性。