项目需要实现预约功能,那么就需要获取用户预约的日期,让用户填写是不明智的选择,因为每个人的写法不一致,如有人喜欢写成“2019.01.01”,而有人喜欢写成“2019-01-01”,还有人喜欢写“2019 年 1 月 1 日”,这样混乱的格式存储在后台中会不方便检索。

那么使用日期选择器来统一格式无论是对用户还是对开发者而言都是更加友好的。

通常情况下我们会使用弹窗来展示日期选择器,我们利用 Android 中内置的控件就可以实现。

DatePicker

DatePicker 实际上就是一个单纯的日期选择器控件,为了使用弹窗展示,我们需要自定义一个 Dialog 来实现,可参考『Android 自定义 Dialog 布局』。

在布局中加入 DatePicker

<?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:gravity="center"
    android:padding="8dp">
    <DatePicker
        android:id="@+id/date_picker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

再设计其响应逻辑:

public static void showDatePicker(Context context) {
    final Calendar calendar = Calendar.getInstance();
    View datePickerView = LayoutInflater.from(context).inflate(R.layout.date_picker_dialog, null);
    DatePicker datePicker = datePickerView.findViewById(R.id.date_picker);
    datePicker.setMinDate(calendar.getTimeInMillis());
    datePicker.init(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH), new DatePicker.OnDateChangedListener() {
        @Override
        public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
            calendar.set(year, monthOfYear, dayOfMonth);
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
            textView.setText(format.format(calendar.getTime()));
//          textView.setText(year + "-" + (monthOfYear + 1) + "-" + dayOfMonth);
        }
    });
    final AlertDialog dialog = new AlertDialog.Builder(context).create();
    dialog.setCanceledOnTouchOutside(false);
    dialog.setCancelable(false);
    dialog.setView(datePickerView);
    dialog.setButton(DialogInterface.BUTTON_POSITIVE, "OK", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
        }
    });
    dialog.show();
}

载入自定义布局后,调用 DatePicker.setMinDate() 方法来设置最小可选择的日期,同理,调用 DatePicker.setMaxDate() 方法来设置最大可选择的日期,另外还有如 DatePicker.setFirstDayOfWeek() 设置每周的第一天等各种配置,需要的时候查查文档即可。

接着就是对 DatePicker 的初始化操作,DatePicker.init() 接收 4 个参数,分别是默认的年月日和 DatePicker 被修改时响应的事件,默认的年月日一般设定为当前时间,响应事件为了方便演示我就用 TextView 来展示这个日期,这里需要注意,建议使用 SimpleDateFormat 来格式化日期,因为月份的计数是从 0 开始的,所以如果直接使用的话要记得加 1

DatePicker 虽然有单独的 setOnDateChangedListener() 方法,但其要求 API 26 才能够使用:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    datePicker.setOnDateChangedListener(new DatePicker.OnDateChangedListener() {
        @Override
        public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
            calendar.set(year, monthOfYear, dayOfMonth);
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
            textView.setText(format.format(calendar.getTime()));
        }
    });
}

如果仅设定了响应事件而没有初始化默认日期,DatePicker 会把当前日期设为默认日期。

然后是关于 AlertDialog 的一些设置,我设置为不可以任何方式取消,并加入一个 PositiveButton 关闭这个 AlertDialog,原因是当用户点击日期的时候 DatePicker 就已响应,而当响应结束之后,没有任何提示告诉用户已经处理完该事件,除非用户点击返回或其他地方关闭该 AlertDialog,所以禁用取消以及使用该 PositiveButton 关闭 AlertDialog 是为了更加友好的交互,当然你也可以在用户点击日期后立即关闭,但我认为这样假如用户误触后的修正步骤变长了。

效果如下:

DatePicker 响应

另外,使用 DatePicker 时建议先在需要用到的地方设定一个默认日期,一般也就是 DatePicker.init() 中设定的日期,因为 DatePicker 仅在日期改变时才会作出响应,如果用户打开 DatePicker 日期选择器后没有点击操作而是直接关闭日期选择器(因为有选择器上默认日期)时,响应事件内的逻辑将得不到执行,也就无法获取到选中的日期(即默认的日期)。如图:

DatePicker 未响应

除了默认的日历卡样式外,DatePicker 还提供了常见的滚轮样式:

DatePicker-Spinner

只需在控件中加入两行属性代码即可:

<DatePicker
        ...
        android:calendarViewShown="false"
        android:datePickerMode="spinner" />

意思就是指定选择器样式为滚轮样式并隐藏日历卡,如果仅设置滚轮样式而不隐藏日历卡样式的话默认是会两个同时存在的:

DatePicker-Spinner&CalendarView

一来没有必要,二来也太丑,特别在竖屏时控件就挤到变形,影响体验。

DatePickerDialog

再来看看 DatePickerDialog,望文生义,DatePickerDialog 就是一个包含了 DatePicker 的对话框,也就是说 Android 帮我们把上面的封装完成了,实际上 DatePickerDialog 在 API 24 才被加进来,使用它有一个好处,就是不需要写布局文件。

public static void showDatePickerDialog(Context context) {
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
        final Calendar calendar = Calendar.getInstance();
        DatePickerDialog dialog = new DatePickerDialog(context);
        dialog.setOnDateSetListener(new DatePickerDialog.OnDateSetListener() {
            @Override
            public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
                calendar.set(year, month, dayOfMonth);
                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
                textView.setText(format.format(calendar.getTime()));
//              textView.setText(year + "-" + (monthOfYear + 1) + "-" + dayOfMonth);
            }
        });
        DatePicker datePicker = dialog.getDatePicker();
        datePicker.setMinDate(calendar.getTimeInMillis());
        dialog.show();
    }
}

DatePicker 不同,DatePickerDialog 有多个构造方法,可以不设定默认的日期,当不设定时,DatePickerDialog 会把当前时间设为默认日期。

DatePickerDialog 继承自 AlertDialog,所以会有 PositiveButtonNegativeButton,因此响应事件回调不再是 onDateChanged() 而是 onDateSet(),即用户点击 PositiveButton 时才会响应事件,把选中的日期回传,这样就不会遇到像 DatePicker 未点击所以获取不到日期的尴尬情况。

DatePickerDialog 比较不方便的是不能够直接设置最小可选日期,所以需要借助 DatePicker 来实现,通过 DatePickerDialog.getDatePicker() 就可以获得 DatePick 实例,再调用 DatePicker 的方法就可以进行相关的操作。

效果如下:

DatePickerDialog

DatePickerDialog 因为不需要布局文件来构建,所以没有像 DatePicker 一样可以切换为滚轮样式的选项。


最后需要提醒的是,由于兼容性问题原生的日期选择器在不同的 Android 版本中样式不一,在 Android 4.x、Android 5.x 以及 Android 6.0 以上的版本中有很大的出入:

各版本 DatePicker 比较

而且原生的日期选择器本身可定制的 API 也较少,如果介意的话请慎用。