因为Google在Android4.x上采用的是holo风格,在5.x及以上采用的Material Design风格,因此从4.x到5.x,Google重构了DatePicker,包括代码和UI。因此这就产生了兼容性问题。android
在4.x上,DatePicker没有Mode的概念,默认就是滚轮和日历并排显示,但可经过xml或者代码,控制只显示滚轮或者只显示日历。可是日历模式下,存在上述bug1的问题,因此与老大商量了一下,考虑到4.x系统占比过小,可使用滚轮模式。api
定制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)的属性来作到的。app
解决上述发现的bug。
要解决兼容性问题,也要解决bug,因此在代码中必须分状况处理。ide
代码量不多,注释也写的很清楚,相信看完就懂了。源码分析
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(0); View header = mContainer.getChildAt(0); header.setVisibility(View.GONE); } else { //bug4:LOLLIPOP和Marshmallow上,使用spinner模式,而后隐藏滚轮,显示日历(spinner模式下的日历没有头部),日历最底部一排日期被截去部分。因此只能使用calender模式,而后手动隐藏header(系统没有提供隐藏header的api)。 //若是只要日历部分,隐藏header ViewGroup mContainer = (ViewGroup) datePicker.getChildAt(0); View header = mContainer.getChildAt(0); header.setVisibility(View.GONE); //Marshmallow上底部留白太多,减少间距 LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) datePicker.getLayoutParams(); layoutParams.bottomMargin = 10; 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,23); endDate.set(Calendar.MINUTE,59); endDate.set(Calendar.SECOND,59); 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(),0,0,0); selectedDate.set(Calendar.MILLISECOND,0); 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(); } }
<?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="1" 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="1" 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>
<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>
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 = 24 * 60 * 60 * 1000; 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,0); currentDate.set(Calendar.MINUTE,0); currentDate.set(Calendar.SECOND,0); currentDate.set(Calendar.MILLISECOND,0); bundle.putSerializable(CustomDatePickerDialogFragment.CURRENT_DATE,currentDate); long start = currentDate.getTimeInMillis() - day * 2; 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+1)+"月"+dayOfMonth+"日",Toast.LENGTH_SHORT).show(); }
<?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>
可见在6.0+上效果最好。字体
除了本身用DialogFragment封装,系统还直接给提供了DatePickerDialog,能够直接以对话框的形式使用,可是这样就不够灵活了。ui
做者:华枯荣
转自:https://www.jianshu.com/p/6700e0422e6ethis