ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android Date Time组件基础:DatePickerDialog与TimePickerDialog实战用法

Android Date  Time组件基础:DatePickerDialog与TimePickerDialog实战用法 做Android开发绕不开的一个需求就是让用户选择日期和时间。从最简单的记事本、闹钟到稍微复杂一点的预约系统、报表筛选底层都离不开Date Time组件这一套东西。我见过不少刚入门的朋友一上来就想着用EditText让用户手输日期结果被各种格式、越权校验搞得焦头烂额。其实Android官方早就给你备好了现成的组件和对话框直接拿来用就行。这篇上篇我先带你把这套Date Time组件的基础用法过一遍把最核心的DatePicker、TimePicker以及配套的对话框方式讲透保证你学完能直接上手用到自己的项目里。这套组件适合谁说实话哪怕你只跟着教程敲过一遍Hello World代码量那种入门水平这篇文章里的东西就够你消化吸收了。我不打算搬一大堆源码贴给你看那没意义。我尽量用写项目时的真实逻辑来讲遇到哪些坑、为什么要这么写都会交代清楚。1. 内容整体设计与思路拆解1.1 Date Time组件到底包含哪些东西Android里涉及日期时间的组件其实是一整个家族。很多人一听到“Date Time组件”就以为只有一个控件这是个误区。官方把这套组件分成几个不同的角色各有各的用处DatePicker日期选择控件让用户从日历视图或者滚轮视图里选出具体的年月日。TimePicker时间选择控件让用户选小时和分钟。CalendarView日历展示控件把整个月份的日期格子展示出来你可以在上面标记、选中某一天。TextClock显示当前时间的文本控件支持自定义时间格式。AnalogClock老式的模拟时钟就是那种带指针的钟表样式现在用得少了但入门阶段认识一下没坏处。这几种控件的定位是有区别的。DatePicker和TimePicker解决的是“输入”问题你给用户一个选择界面用户选完之后代码能拿回选中的值CalendarView更像是一种“展示选择”的组合控件通常用来做带标记的日历界面比如显示哪几天有打卡记录TextClock和AnalogClock则是纯粹的“展示”控件基本就是拿来显示时间用的。我的建议是入门阶段你的精力应该重点放在DatePicker、TimePicker以及它们对应的对话框版本上。因为你做项目百分之八十的需求是“让用户选一个时间”而不是“让用户看一个日历”。先把选定和取值这个闭环打通后面再玩CalendarView那种自由度高一点的控件心理负担会小很多。1.2 为什么要优先掌握原生组件和对话框初学阶段最容易犯的毛病就是“什么都要自己写”。我之前见过一个学员非要自己用GridView做一个日期选择器折腾了一周bug一堆最后跑来找我我给他换成了系统自带的DatePickerDialog十分钟搞定。这不是说你自己写就完全不行而是“先会用现成的再想着DIY”。官方提供的DatePickerDialog和TimePickerDialog在大多数主流机型上表现稳定适配成本低交互也是用户熟悉的风格。你用第三方开源库还得考虑依赖版本、主题风格、体积增量这些问题。初学阶段用原生的最稳。另外还有一层考虑Android系统版本多、厂商定制多你手写一个日期控件很容易在某个小众机型上出现布局错乱而官方原生控件在系统层面做了大量兼容工作。说得直白一点原生组件就是“兜底方案”你找不出理由不用它。2. 核心细节解析与实操要点2.1 DatePicker的核心用法与取值逻辑DatePicker在XML里声明非常简单一般放在LinearLayout或者FrameLayout这类容器里DatePicker android:idid/datePicker android:layout_widthwrap_content android:layout_heightwrap_content /默认情况下DatePicker会根据系统版本自动选择显示模式。4.0之前的设备可能会显示成一组下拉框4.0之后大部分是日历视图。如果你在不同的真机上看到样式不一样不要慌这是正常的跟控件自身逻辑无关。在代码里拿值和监听变化是入门的重点。旧版本常用的写法是DatePicker datePicker findViewById(R.id.datePicker); datePicker.init(2024, 5, 18, new DatePicker.OnDateChangedListener() { Override public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) { // 这里注意monthOfYear是从0开始计数的 Log.d(TAG, 选中日期: year - (monthOfYear 1) - dayOfMonth); } });这段代码里藏着新手最容易踩的第一个大坑monthOfYear的范围是0到110表示1月。如果你直接拿monthOfYear去拼接字符串出来的日期总是比实际少一个月。我在实际项目中不止一次排查过这种问题最后发现都是“月份忘记加一”造成的。等到用户操作完控件你还需要主动去获取当前选中的值而不是只在回调里处理。获取方法如下DatePicker datePicker findViewById(R.id.datePicker); int year datePicker.getYear(); int month datePicker.getMonth() 1; // 记得加1 int day datePicker.getDayOfMonth();这套API从很早的Android版本就有了兼容性很好。不过需要注意Android 12API 31左右官方在部分方法上做了微调但核心的获取逻辑没变。2.2 TimePicker的核心用法与24小时制TimePicker和DatePicker的使用节奏差不多。XML声明TimePicker android:idid/timePicker android:layout_widthwrap_content android:layout_heightwrap_content android:is24HourViewtrue /is24HourView这个属性很关键它决定显示成“14:30”还是“下午2:30”。你如果直接写死true不管系统语言环境如何都会展示成24小时制。如果想跟随系统就别写这个属性代码里也不用管。代码层面的取值和监听基础写法是这样TimePicker timePicker findViewById(R.id.timePicker); timePicker.setOnTimeChangedListener(new TimePicker.OnTimeChangedListener() { Override public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { Log.d(TAG, 当前时间: hourOfDay : minute); } });这里有一个历史版本的兼容问题值得单独说。在Android 5.0之前TimePicker获取当前选中小时的标准方法叫getCurrentHour()分钟是getCurrentMinute()。但在Android 5.0API 21之后这两个方法就废弃了官方建议改用getHour()和getMinute()。所以你写代码的时候一定要确认项目的minSdkVersion。如果最低支持到Android 4.x你得做版本判断否则直接在旧设备上调用getHour()会报错。不过现在的应用基本minSdk都到21以上了大家知道有这回事就行。2.3 更推荐的Dialog方式DatePickerDialog和TimePickerDialog真正做项目时我很少把DatePicker直接裸放在页面里占一整块位置更多是用对话框弹出来让用户选完就关闭。这种情况下用DatePickerDialog和TimePickerDialog就顺手得多。DatePickerDialog的用法非常经典Calendar calendar Calendar.getInstance(); int year calendar.get(Calendar.YEAR); int month calendar.get(Calendar.MONTH); int day calendar.get(Calendar.DAY_OF_MONTH); DatePickerDialog dialog new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() { Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { // monthOfYear依然是从0开始的要1 String selectedDate year - (monthOfYear 1) - dayOfMonth; Toast.makeText(MainActivity.this, selectedDate, Toast.LENGTH_SHORT).show(); } }, year, month, day); dialog.show();这里有几个细节要提一下。第一个是回调方法onDateSet很多新手搞不清在什么地方触发。实际运行中用户点击对话框上的“确定”按钮这个方法才会被调用。如果用户点了“取消”或者点了对话框外区域让对话框消失这个回调是不会执行的。所以不要指望在这个回调里处理“用户什么都没选”的情况。TimePickerDialog的思路一模一样Calendar calendar Calendar.getInstance(); int hour calendar.get(Calendar.HOUR_OF_DAY); int minute calendar.get(Calendar.MINUTE); TimePickerDialog dialog new TimePickerDialog(this, new TimePickerDialog.OnTimeSetListener() { Override public void onTimeSet(TimePicker view, int hourOfDay, int minute) { String selectedTime hourOfDay : minute; Toast.makeText(MainActivity.this, selectedTime, Toast.LENGTH_SHORT).show(); } }, hour, minute, true); dialog.show();最后一个参数传的是is24HourView和XML里的意思一样。如果传false显示出来的就是12小时制带AM/PM。我建议你在这个阶段就把对话框方式练熟。因为它不仅代码量少、逻辑清晰而且在各种界面布局下都不占地方用户体验也好。项目里最常见的组合是一个EditText或者TextView点击后弹日期选择对话框选定后把格式化的字符串回填进去。3. 实操过程与核心环节实现3.1 一个完整的组合案例点击文本框弹出日期时间选择器为了让你能直接“抄作业”我写一个比较完整的小例子。功能不复杂页面上有两个输入框一个点击弹日期选择一个点击弹时间选择选完之后回填显示。先看布局文件activity_main.xml?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp EditText android:idid/etDate android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请选择日期 android:focusablefalse android:inputTypenone / EditText android:idid/etTime android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop16dp android:hint请选择时间 android:focusablefalse android:inputTypenone / /LinearLayout注意我给两个EditText设置了focusablefalse和inputTypenone这是刻意为之。如果你不设置一点EditText就会弹出软键盘跟我们的需求冲突。我们想让点击行为变成“弹日期对话框”而不是“弹键盘输入文字”。MainActivity的代码public class MainActivity extends AppCompatActivity { private EditText etDate; private EditText etTime; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etDate findViewById(R.id.etDate); etTime findViewById(R.id.etTime); etDate.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { showDatePickerDialog(); } }); etTime.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { showTimePickerDialog(); } }); } private void showDatePickerDialog() { Calendar calendar Calendar.getInstance(); int year calendar.get(Calendar.YEAR); int month calendar.get(Calendar.MONTH); int day calendar.get(Calendar.DAY_OF_MONTH); DatePickerDialog dialog new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() { Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { String dateStr year - (monthOfYear 1) - dayOfMonth; etDate.setText(dateStr); } }, year, month, day); dialog.show(); } private void showTimePickerDialog() { Calendar calendar Calendar.getInstance(); int hour calendar.get(Calendar.HOUR_OF_DAY); int minute calendar.get(Calendar.MINUTE); TimePickerDialog dialog new TimePickerDialog(this, new TimePickerDialog.OnTimeSetListener() { Override public void onTimeSet(TimePicker view, int hourOfDay, int minute) { String timeStr String.format(Locale.CHINA, %02d:%02d, hourOfDay, minute); etTime.setText(timeStr); } }, hour, minute, true); dialog.show(); } }这个例子里有几个细节你再感受一下。第一个TimePickerDialog回调里我用了String.format(Locale.CHINA, %02d:%02d, hourOfDay, minute)。%02d的意思是如果数字只有一位数前面补一个0保证“09:05”这种显示效果。你直接用“hour “:” minute”也不是不能用但会显示成“9:5”用户看起来不够整齐。第二个日期选择对话框我没做格式化直接拼接year “-” (monthOfYear1) “-” dayOfMonth。这里month加1的坑我又强调了一遍因为实在是太多人折在这上面了。3.2 如何格式化日期时间的更多方式实际项目中我们很少直接把年月日分开拼接因为产品的显示要求千奇百怪有的是“2024-06-18”有的是“2024年06月18日”还有的是“周一 18:30”。这时候就得靠SimpleDateFormat或者项目里封装的时间工具类。一个最常见的做法是先把用户的输入组合成Date对象再按格式输出。代码如下private String formatDateString(int year, int month, int day) { Calendar calendar Calendar.getInstance(); calendar.set(year, month, day); // 注意这里的month也是从0开始 SimpleDateFormat sdf new SimpleDateFormat(yyyy年MM月dd日, Locale.CHINA); return sdf.format(calendar.getTime()); }用Calendar.set()的时候month同样要传“0到11”的范围也就是你在DatePickerDialog回调里拿到的monthOfYear直接传进去就行千万别再减1。我在代码评审时见过有人这么写calendar.set(year, monthOfYear - 1, dayOfMonth)结果日期又往前挪了一个月典型的“好心办坏事”。如果你要拼接日期和时间可以把日期和时间分开存入两个String变量最后再合并。但如果你想用一个变量存下来就得先把年月日时分秒全部set进Calendar再统一格式化像这样Calendar calendar Calendar.getInstance(); calendar.set(year, monthOfYear, dayOfMonth, hourOfDay, minute); String result new SimpleDateFormat(yyyy-MM-dd HH:mm, Locale.CHINA) .format(calendar.getTime());注意SimpleDateFormat里的HH是大写表示24小时制的小时如果是hh小写那是12小时制的意思配合“a”才能显示上午下午。这个大小写太容易搞混了我建议你干脆记住“大写HH24小时制”这个规律就好。3.3 设置默认显示日期的几种思路有时候需求是“用户一点开日期对话框默认显示在某个特定日期上”比如从后台配置里读到的目标日期而不是今天的日期。实现方式其实很简单就是在创建DatePickerDialog的时候把date参数替换掉。举个例子假设后端下发了一个时间戳字符串你要把它解析成默认日期。简化流程如下String timestampStr 1718700000000; // 示例时间戳 long timestamp Long.parseLong(timestampStr); Calendar calendar Calendar.getInstance(); calendar.setTimeInMillis(timestamp); int defaultYear calendar.get(Calendar.YEAR); int defaultMonth calendar.get(Calendar.MONTH); int defaultDay calendar.get(Calendar.DAY_OF_MONTH); DatePickerDialog dialog new DatePickerDialog(this, listener, defaultYear, defaultMonth, defaultDay); dialog.show();这里有个常见的坑有些开发者在Calendar设置完时间戳之后没有调用setTimeInMillis还是默认的当前时间那自然就显示成了今天。代码看似没问题但结果是错的。这种问题你自己排查起来非常花时间最好的办法是在创建对话框前打一行日志把defaultYear、defaultMonth、defaultDay打出来一眼就能看出问题。TimePicker默认时间的设置也是同理在创建TimePickerDialog时传入hourOfDay和minute参数就可以。4. 常见问题与排查技巧实录4.1 月份始终少一个月怎么排查这个问题我几乎每次带新人都会遇到。现象就是用户选了6月18号显示出来却是5月18号。根因就是回调里的monthOfYear从0开始计数你没有加1。从代码层面排查其实很简单。你只需要在回调的第一行加个LogLog.d(DEBUG, year year , monthOfYear monthOfYear , day dayOfMonth);然后看Logcat的输出。如果你选了6月日志里monthOfYear打印出来是5那基本就是这个原因把monthOfYear加1即可。有一种比较隐蔽的情况第三方库或封装好的工具类里可能已经帮你加了1你又在外层加了一次导致结果反过来变成7月。问清楚工具类的入参范围或者直接翻它的源码确认一下是最稳妥的做法。这个“两层加一”的问题比“忘记加一”更难发现因为它不会导致日期明显错乱只有细心对比才能看出来。4.2 回调不触发点了确定没反应另一个高发问题是用户点击对话框的确定按钮但onDateSet回调没有执行。这个情况多半不是因为时间监听写错而是你用了旧版本的构造方法或者传了错误的主题。在Android 5.0之前你可以直接new DatePickerDialog(context, listener, year, month, day)。但在更高版本上有些厂商设备对Dialog的主题展示有冲突导致按钮不可见或者点击区域异常。建议你显式指定主题像这样DatePickerDialog dialog new DatePickerDialog(this, AlertDialog.THEME_HOLO_LIGHT, listener, year, month, day);多传一个主题参数之后大部分机型的显示和点击问题都能缓解。如果你用的是Material Components库可以换成MaterialDatePicker不过那是另一个话题了基础阶段先将原生控件用明白。还有一个小概率情况你在监听器里做了耗时操作或者抛了异常导致回调虽然触发了但程序闪退表面上看起来“没反应”。检查一下有没有try-catch黏在回调里避免因为空指针导致整个对话框崩溃。4.3 显示样式在不同机型上差异太大要不要处理这个问题不是bug但很多入门的同学会当成bug来问。同样是DatePicker在小米上显示成滚轮在三星上显示成日历在模拟器上又是另一种样子以为是自己代码写错了。真相很简单Android系统版本的演进过程中DatePicker的默认样式经历了数次调整各厂商ROM也会在原生基础上做二次定制。所以不同设备显示不同是常态不需要你去“修复”。如果你希望从代码层面固定显示模式可以调用DatePicker的setCalendarViewShown(false)等方法来控制子组件的显隐。但我要提醒你这种强制控制在不同ROM上的表现并不完全一致强行去调反而可能出现一些奇怪的显示问题。我的建议是放平心态接受系统的呈现方式。4.4 对话框被点击外部区域关闭误操作丢数据DatePickerDialog默认是允许点击对话框外部区域关闭的。如果用户在编辑表单时误触选好的日期没保存就直接消失了产品经理大概率会来找你“优化交互”。解决办法也很直接在show()之前加一句dialog.setCanceledOnTouchOutside(false);这句话让对话框不会因为点击外部而消失用户必须明确点击确定或取消按钮。另外你还可以覆写onCancel或者OnCancelListener在用户取消时记录一下当前状态方便恢复现场。多说一句设置了这个为false之后物理返回键依然可以关闭对话框返回键对应的回调是onCancel不会触发onDateSet。如果你要感知“用户取消”这个行为就实现DialogInterface.OnCancelListener。4.5 时区问题导致默认时间不对有一种情况我很早就遇到过用户手机设置的时区是“UTC8”但代码里用SimpleDateFormat格式化时用的默认时区而模拟器的默认时区跟真机不一样结果时间显示差了几个小时。时区问题的本质是Calendar实例和SimpleDateFormat如果不显式指定时区默认会取系统时区。如果后端下发的时间戳是UTC时间你直接用getTime()格式化出来会看到偏移后的时间。稳妥的做法是在格式化前指定时区SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm, Locale.CHINA); sdf.setTimeZone(TimeZone.getTimeZone(GMT08:00));这个写法会强制把时间输出成东八区的时间无论用户手机设置成什么时区显示结果都是一致的。如果你是做面向国内用户的应用这个处理基本够用了。当然如果应用涉及海外用户就要更细致地处理时区转换了但那是进阶话题这里先知道这个坑存在就好。5. 最适合入门阶段的练习方向学到这里你已经能熟练使用DatePickerDialog和TimePickerDialog了。我建议你做一个小练习来巩固做一个“生日信息填写”页面点击一个按钮弹出日期选择器选完自动回填到TextView并且显示“你选择的生日是1995年08月20日距今XX年”。这个练习会把日子取值、格式化、计算间隔年份全部串起来非常有利于检验学习成果。进阶一点的练习是去做一个“闹钟设置页”用TimePickerDialog选时间然后把这个时间保存到SharedPreferences里下次打开页面还能回显出来。这个练习适合培养“数据持久化”的意识也是项目开发里非常常用的组合。另外一个方向是研究一下CalendarView把它嵌在一个页面里尝试监听用户点击日期的事件。CalendarView有一个回调方法setOnDateChangeListener用法也很简单。但我不建议初学阶段花太多时间在这上面因为它的定制空间很大一旦钻进去想改样式就容易掉进自定义View的坑里那已经不属于入门范畴了。如果还有余力可以看看Material Components库里的MaterialDatePicker。这个控件在现代项目里越来越流行动画和风格比原生Dialog更统一。但它的API结构、引入依赖的方式又跟原生不同属于另一套体系。先把原生的学扎实再迁移过去思路会顺畅很多。我教过的学生里凡是老老实实把上面三个练习做完的后面去做涉及日期时间的真实功能基本不会再犯“月份少一”或者“点确定没反应”这种低级错误。怕就怕一上来就到处找开源库、复制粘贴一堆不知道原理的代码出了问题完全不知道从哪排查。这跟学数学一个道理你得先会手算才能理解计算器为什么可靠。
返回列表