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方法获取并修改隐藏属性,很有启发性。

android 日期控件 DatePicker的更多相关文章

  1. 第32讲 UI组件之 时间日期控件DatePicker和TimePicker

    第32讲 UI组件之 时间日期控件DatePicker和TimePicker 在Android中,时间日期控件相对来说还是比较丰富的.其中, DatePicker用来实现日期输入设置,    Time ...

  2. jQuery UI 日期控件--datepicker

    在web开发中,日期的输入经常会遇到.我们会用的解决方法有: 1.自己写css和js,对日期进行控制:----有点浪费精力和时间: 2.用easyui插件中的日期插件来实现: 3.用juqery-ui ...

  3. mvc4中jquery-ui日期控件datepicker的应用

    mvc4中jquery-ui日期控件datepicker的应用 本文适合mvc中日期选择需要的同学: 假设读者已经具备了mvc4和javascript中的相关知识 一. 开始项目之前把项目中目录:/C ...

  4. 日期控件 DatePicker 在ie8不能用

    过个年,日期控件DatePicker在ie8下突然不能用了,程序也没升级,很是奇怪. 把ie8的“禁用脚本调试”去掉,再次运行,发现提示有脚本错误. 想着可能是兼容性问题,于是把兼容性视图打开运行,还 ...

  5. bootstrop 日期控件 datepicker被弹出框dialog覆盖的解决办法

    筒子们在使用bootstrap的日期控件(datepicker , 现在官网提供的名称叫 datetimepicker)时可能会遇到如上图的问题这是啥原因造成的呢? 答案很简单时输出的优先级造成的(z ...

  6. 日期控件datepicker的使用

    引入JS: <script type="text/javascript" src="static/my/js/bootstrap-datepicker.min.js ...

  7. Android --日期控件使用并显示选择的日期

    1. main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns: ...

  8. Android基础控件DatePicker的使用

    1.简介 DatePicker日期选择器,自带spinner和calendar两种模式,相关属性: android:calendarTextColor : 日历列表的文本的颜色 android:cal ...

  9. 使用日期控件datePicker,阻止移动端的自动调取键盘的事件

    方法:简单来说就是阻止input的默认事件. 因为datePicker就是用input来封装的,所以直接阻止input的输入事件就ok: 很简单,把input field属性readonly设置为tr ...

随机推荐

  1. tfs查看最近签入记录及文件

    在团队资源管理=>源代码管理资源管理器=>选择某个最近签入的文件夹=>右键=>查看历史记录=>双击某个文件夹 就能看到最近变更集文件

  2. 神经网络的BP算法

    正向传播: W下脚标定义根据用户自己的习惯 反向传播算法 1.误差由本层传到上层相关联的结点,权重分配 2.上层某个结点的总误差 2.误差最小化与权重变量有关,最小梯度法. 权重因子更新 偏导数求解, ...

  3. 关于GROUP BY和聚合函数

    可以这样去理解group by和聚合函数 转自 http://www.cnblogs.com/wiseblog/articles/4475936.html 写在前面的话:用了好久group by,今天 ...

  4. webApp在各大Android市场上的发布

    本来打算每个月都写上一篇博客的,可是计划永远赶不上变化,不过这其中也有自己的懒惰,果然过年让整个人懈怠了不少.年后一直在赶项目以致于到今天才动手写这篇文章. 这一篇主要写点在公司的要求下发布的webA ...

  5. 使用IST重新加入节点(5.7.20)

    IST不是SST用于节点重新加入吗?我们有解决方案! 鉴于上述痛点,我们将介绍 gcache.freeze_purge_at_seqno Percona XtraDB Cluster 5.7.20.这 ...

  6. python的datetime常用方法

    把datetime转成字符串 datetime.strftime("%Y-%m-%d-%H") 把字符串转成datetime datetime.strptime(datetime, ...

  7. iOS UIFileSharingEnabled

    一.让iOS App通过iTunes进行文件交换Documents 让iOS App通过iTunes进行文件交换 有一些App需要通过使用iTunes让用户上传和下载文档.要让iOS程序支持iTune ...

  8. vue教程1-05 事件 简写、事件对象、冒泡、默认行为、键盘事件

    vue教程1-05 事件 简写.事件对象.冒泡.默认行为.键盘事件 v-on:click/mouseover...... 简写的: @click="" 推荐 事件对象: @clic ...

  9. 解决 在 WINDOWS 下 同时安装 python2 python3 后 pip 错误

    再之前同时安装 python 后 只需把环境变量PATH 里面改为 PATH=C:\Python36-32\Scripts\;C:\Python36-32\;C:\Python27\;C:\Pytho ...

  10. PyCharm下载与激活

    1.集成开发环境(IDE:Integrated Development Environment)PyCharm下载地址:https://www.jetbrains.com/pycharm/downlo ...