|
|
|
|
@ -14,157 +14,176 @@
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// 包声明:归属小米便签的UI模块,自定义日期时间选择控件核心类
|
|
|
|
|
// 包声明:归属小米便签UI模块,日期时间选择的核心自定义复合控件,供弹窗调用
|
|
|
|
|
package net.micode.notes.ui;
|
|
|
|
|
|
|
|
|
|
// 导入日期格式化工具类:处理星期/日期的文本展示
|
|
|
|
|
// Java文本格式化工具类,获取系统的上午/下午文本标识,适配多语言环境
|
|
|
|
|
import java.text.DateFormatSymbols;
|
|
|
|
|
// 导入日历类:核心的日期时间管理,处理年/月/日/时/分的计算与联动
|
|
|
|
|
// Java核心日历工具类,全局唯一的时间数据载体,处理所有年月日时分的计算、联动、转换逻辑
|
|
|
|
|
import java.util.Calendar;
|
|
|
|
|
|
|
|
|
|
// 导入资源类:引用布局文件(datetime_picker.xml)
|
|
|
|
|
// 小米便签资源文件引用,获取布局、字符串等资源ID常量
|
|
|
|
|
import net.micode.notes.R;
|
|
|
|
|
|
|
|
|
|
// 导入安卓上下文类:创建控件、加载资源
|
|
|
|
|
// 安卓系统核心类 - 上下文,提供控件创建、资源加载、布局填充的运行环境
|
|
|
|
|
import android.content.Context;
|
|
|
|
|
// 导入日期格式化工具:判断系统24小时制设置
|
|
|
|
|
// 安卓系统日期格式化工具类,提供系统24小时制判断、日期文本格式化能力
|
|
|
|
|
import android.text.format.DateFormat;
|
|
|
|
|
// 导入视图相关类:布局容器、视图可见性控制
|
|
|
|
|
// 安卓视图体系核心类,视图基础属性配置、可见性控制、布局容器核心父类
|
|
|
|
|
import android.view.View;
|
|
|
|
|
import android.widget.FrameLayout;
|
|
|
|
|
// 导入数字选择器:核心的日期/时间选择UI组件
|
|
|
|
|
// 安卓数字滚轮选择器,本控件的核心子组件,实现年月日时分的滚轮滑动选择UI
|
|
|
|
|
import android.widget.NumberPicker;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 自定义日期时间选择控件
|
|
|
|
|
* 核心特性:
|
|
|
|
|
* 1. 布局组成:集成日期(近7天)、小时、分钟、上午/下午(AM/PM)四个NumberPicker;
|
|
|
|
|
* 2. 时间联动:处理时间选择的边界联动(如分钟59→0时小时+1、小时23→0时日期+1);
|
|
|
|
|
* 3. 制式适配:支持24小时制/12小时制切换,自动适配系统默认设置;
|
|
|
|
|
* 4. 回调通知:时间选择变化时触发回调,传递最新的年/月/日/时/分;
|
|
|
|
|
* 5. 状态控制:支持整体启用/禁用所有选择器,统一管理交互状态;
|
|
|
|
|
* 典型使用场景:便签提醒时间设置的DateTimePickerDialog中作为核心选择UI。
|
|
|
|
|
* 自定义日期时间复合选择控件【核心基础UI控件】
|
|
|
|
|
* 继承:FrameLayout 帧布局,作为复合控件的根布局容器,承载多个子选择器
|
|
|
|
|
* 核心定位:小米便签「提醒时间设置」功能的底层核心控件,被DateTimePickerDialog弹窗集成使用
|
|
|
|
|
* 核心设计理念:将「日期选择+小时选择+分钟选择+上下午选择」整合为统一控件,封装所有时间联动逻辑、格式适配逻辑、数据处理逻辑,对外提供极简的调用与通信接口
|
|
|
|
|
* 核心特性与职责:
|
|
|
|
|
* 1. 布局整合:内置日期、小时、分钟、上下午共4个NumberPicker滚轮选择器,形成一体化的时间选择UI;
|
|
|
|
|
* 2. 数据闭环:基于Calendar类做全局唯一的时间数据管理,所有选择操作最终同步至该对象,保证数据一致性;
|
|
|
|
|
* 3. 智能联动:完美处理所有时间边界联动场景,如分钟59→0时小时+1、小时23→0时日期+1、12小时制跨上下午切换等,无数据断层;
|
|
|
|
|
* 4. 系统适配:自动识别并适配系统的24小时制/12小时制设置,支持手动强制切换,两种制式无缝兼容,交互无感知;
|
|
|
|
|
* 5. 日期展示:固定展示近7天的日期列表,格式为「月.日 星期」,满足便签短周期提醒的业务需求;
|
|
|
|
|
* 6. 状态统一:支持控件整体启用/禁用,一键同步所有子选择器的交互状态,无需单独配置;
|
|
|
|
|
* 7. 标准化通信:提供时间变化的回调接口,选择操作实时触发回调,向外传递标准化的年月日时分数据;
|
|
|
|
|
* 8. 细节优化:分钟选择器支持长按快速滚动、上下午文本适配系统多语言、时间数值边界校验等细节体验优化;
|
|
|
|
|
* 9. 防抖动处理:初始化阶段屏蔽回调触发,避免初始化时的无效数据通知,提升性能与稳定性。
|
|
|
|
|
* 核心优势:控件内聚性极强,所有时间相关的逻辑全部封装内部,外部调用方只需关心「设置初始时间」「获取选中时间」「监听时间变化」三个核心操作,完全无需处理内部复杂逻辑。
|
|
|
|
|
* 典型业务场景:唯一使用场景为DateTimePickerDialog弹窗的核心内容视图,支撑便签的提醒时间选择功能。
|
|
|
|
|
*/
|
|
|
|
|
public class DateTimePicker extends FrameLayout {
|
|
|
|
|
|
|
|
|
|
// ======================== 基础常量 - 控件默认状态 ========================
|
|
|
|
|
/** 控件默认启用状态:初始为启用 */
|
|
|
|
|
// ======================== 基础常量区 - 控件默认配置与固定数值 ========================
|
|
|
|
|
/** 控件默认启用状态:初始化时默认所有选择器均可交互 */
|
|
|
|
|
private static final boolean DEFAULT_ENABLE_STATE = true;
|
|
|
|
|
|
|
|
|
|
// ======================== 常量 - 时间数值范围 ========================
|
|
|
|
|
/** 半天的小时数:12小时制的核心数值(AM/PM分界) */
|
|
|
|
|
// ======================== 常量区 - 时间单位核心数值 ========================
|
|
|
|
|
/** 半天小时数:12小时制的核心分界值,上午/下午的小时数上限 */
|
|
|
|
|
private static final int HOURS_IN_HALF_DAY = 12;
|
|
|
|
|
/** 全天的小时数:24小时制的核心数值 */
|
|
|
|
|
/** 全天小时数:24小时制的核心数值,一天的总小时数 */
|
|
|
|
|
private static final int HOURS_IN_ALL_DAY = 24;
|
|
|
|
|
/** 一周的天数:日期选择器展示近7天 */
|
|
|
|
|
/** 一周天数:日期选择器固定展示的天数,不可修改,适配短周期提醒业务 */
|
|
|
|
|
private static final int DAYS_IN_ALL_WEEK = 7;
|
|
|
|
|
|
|
|
|
|
// ======================== 常量 - NumberPicker取值范围 ========================
|
|
|
|
|
/** 日期选择器最小值:0(对应近7天的起始索引) */
|
|
|
|
|
// ======================== 常量区 - NumberPicker滚轮选择器 取值范围约束 ========================
|
|
|
|
|
/** 日期选择器-最小值:固定为0,对应近7天列表的第一条数据 */
|
|
|
|
|
private static final int DATE_SPINNER_MIN_VAL = 0;
|
|
|
|
|
/** 日期选择器最大值:6(对应近7天的结束索引,DAYS_IN_ALL_WEEK - 1) */
|
|
|
|
|
/** 日期选择器-最大值:固定为6,对应近7天列表的最后一条数据,数值等于天数减1 */
|
|
|
|
|
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
|
|
|
|
|
|
|
|
|
|
/** 24小时制-小时选择器最小值:0(凌晨0点) */
|
|
|
|
|
/** 24小时制-小时选择器-最小值:凌晨0点,时间起点 */
|
|
|
|
|
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
|
|
|
|
|
/** 24小时制-小时选择器最大值:23(深夜23点) */
|
|
|
|
|
/** 24小时制-小时选择器-最大值:深夜23点,时间终点 */
|
|
|
|
|
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
|
|
|
|
|
|
|
|
|
|
/** 12小时制-小时选择器最小值:1(上午/下午1点) */
|
|
|
|
|
/** 12小时制-小时选择器-最小值:上午/下午的1点,无0点概念 */
|
|
|
|
|
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
|
|
|
|
|
/** 12小时制-小时选择器最大值:12(上午/下午12点) */
|
|
|
|
|
/** 12小时制-小时选择器-最大值:上午/下午的12点,封顶数值 */
|
|
|
|
|
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
|
|
|
|
|
|
|
|
|
|
/** 分钟选择器最小值:0(整点) */
|
|
|
|
|
/** 分钟选择器-最小值:整点0分,分钟起点 */
|
|
|
|
|
private static final int MINUT_SPINNER_MIN_VAL = 0;
|
|
|
|
|
/** 分钟选择器最大值:59(整点前1分钟) */
|
|
|
|
|
/** 分钟选择器-最大值:整点前1分钟,59分,分钟终点 */
|
|
|
|
|
private static final int MINUT_SPINNER_MAX_VAL = 59;
|
|
|
|
|
|
|
|
|
|
/** 上午/下午选择器最小值:0(AM/上午) */
|
|
|
|
|
/** 上下午选择器-最小值:0,对应上午/AM */
|
|
|
|
|
private static final int AMPM_SPINNER_MIN_VAL = 0;
|
|
|
|
|
/** 上午/下午选择器最大值:1(PM/下午) */
|
|
|
|
|
/** 上下午选择器-最大值:1,对应下午/PM */
|
|
|
|
|
private static final int AMPM_SPINNER_MAX_VAL = 1;
|
|
|
|
|
|
|
|
|
|
// ======================== 成员变量 - UI组件 ========================
|
|
|
|
|
/** 日期选择器:展示近7天的日期(格式如“12.23 星期二”) */
|
|
|
|
|
// ======================== 成员变量区 - UI核心组件【所有子选择器,全局持用避免重复查找】 ========================
|
|
|
|
|
/** 日期滚轮选择器:核心展示近7天的格式化日期文本,如「01.15 周四」,支持滑动切换日期 */
|
|
|
|
|
private final NumberPicker mDateSpinner;
|
|
|
|
|
/** 小时选择器:根据24/12小时制展示不同范围的小时数 */
|
|
|
|
|
/** 小时滚轮选择器:根据24/12小时制展示对应范围的小时数,核心时间选择组件 */
|
|
|
|
|
private final NumberPicker mHourSpinner;
|
|
|
|
|
/** 分钟选择器:0~59的分钟数选择 */
|
|
|
|
|
/** 分钟滚轮选择器:固定0~59的分钟数选择,支持长按快速滚动,核心时间选择组件 */
|
|
|
|
|
private final NumberPicker mMinuteSpinner;
|
|
|
|
|
/** 上午/下午选择器:仅12小时制显示,0=AM/上午,1=PM/下午 */
|
|
|
|
|
/** 上下午滚轮选择器:仅12小时制显示,0=上午/AM,1=下午/PM,适配12小时制的时间展示 */
|
|
|
|
|
private final NumberPicker mAmPmSpinner;
|
|
|
|
|
|
|
|
|
|
// ======================== 成员变量 - 日期时间状态 ========================
|
|
|
|
|
/** 核心日历对象:存储当前选中的日期时间,处理所有时间计算/联动 */
|
|
|
|
|
// ======================== 成员变量区 - 核心状态与数据载体【全局核心数据,控件的大脑】 ========================
|
|
|
|
|
/** 核心日历对象:全局唯一的时间数据载体,存储当前选中的完整年月日时分信息,所有选择操作最终同步至此,所有外部获取操作均来源于此,保证数据唯一可信 */
|
|
|
|
|
private Calendar mDate;
|
|
|
|
|
/** 日期选择器展示文本数组:存储近7天的格式化日期文本(如“12.23 星期二”) */
|
|
|
|
|
/** 日期展示文本数组:缓存近7天的格式化日期文本,供日期选择器展示使用,避免重复计算 */
|
|
|
|
|
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
|
|
|
|
|
/** 上午/下午标记:true=AM/上午,false=PM/下午(仅12小时制有效) */
|
|
|
|
|
/** 上下午状态标记:true=上午/AM,false=下午/PM,仅在12小时制下生效,控制时间计算与展示 */
|
|
|
|
|
private boolean mIsAm;
|
|
|
|
|
/** 24小时制标记:true=24小时制,false=12小时制 */
|
|
|
|
|
/** 24小时制状态标记:true=启用24小时制,隐藏上下午选择器;false=启用12小时制,显示上下午选择器 */
|
|
|
|
|
private boolean mIs24HourView;
|
|
|
|
|
/** 控件启用状态:true=启用,false=禁用所有选择器 */
|
|
|
|
|
/** 控件整体启用状态标记:true=所有选择器可交互,false=所有选择器禁用,统一管控交互权限 */
|
|
|
|
|
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
|
|
|
|
|
/** 初始化标记:true=控件正在初始化,避免初始化时触发不必要的回调 */
|
|
|
|
|
/** 初始化状态标记:true=控件正在初始化,屏蔽所有回调触发;false=初始化完成,正常响应所有操作与回调,防止初始化阶段的无效数据通知 */
|
|
|
|
|
private boolean mInitialising;
|
|
|
|
|
|
|
|
|
|
// ======================== 成员变量 - 回调监听 ========================
|
|
|
|
|
/** 日期时间变化回调监听器:外部实现,接收时间变化通知 */
|
|
|
|
|
// ======================== 成员变量区 - 回调通信接口 ========================
|
|
|
|
|
/** 日期时间变化的回调监听器:外部实现该接口,接收控件的时间变化通知,是控件与外部通信的唯一桥梁 */
|
|
|
|
|
private OnDateTimeChangedListener mOnDateTimeChangedListener;
|
|
|
|
|
|
|
|
|
|
// ======================== 成员变量 - NumberPicker值变化监听器 ========================
|
|
|
|
|
/** 日期选择器值变化监听器:处理日期切换,更新日历对象并触发回调 */
|
|
|
|
|
// ======================== 成员变量区 - 滚轮选择器 值变化监听器【所有选择器的核心交互逻辑,内部闭环】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 日期选择器 值变化监听器:处理日期滑动切换的核心逻辑
|
|
|
|
|
* 核心能力:监听日期选择器的数值变化,计算日期偏移量,同步更新核心日历对象的日期,刷新日期展示文本,最终触发时间变化回调
|
|
|
|
|
* 无边界特殊处理:日期选择器固定展示近7天,滑动仅在7天内切换,无需处理跨月跨年的复杂逻辑
|
|
|
|
|
*/
|
|
|
|
|
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
|
|
|
|
|
@Override
|
|
|
|
|
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
|
|
|
|
|
// 调整日历的天数(新值-旧值为天数偏移量)
|
|
|
|
|
// 计算日期偏移量:新值-旧值 即为需要增减的天数,直接同步至核心日历对象
|
|
|
|
|
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
|
|
|
|
|
// 更新日期选择器的展示文本
|
|
|
|
|
// 刷新日期选择器的展示文本,保证文本与选中日期一致
|
|
|
|
|
updateDateControl();
|
|
|
|
|
// 触发日期时间变化回调
|
|
|
|
|
// 触发全局时间变化回调,向外通知日期已更新
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 小时选择器值变化监听器:处理小时切换,包含边界联动(小时→日期)和12/24小时制适配 */
|
|
|
|
|
/**
|
|
|
|
|
* 小时选择器 值变化监听器:处理小时滑动切换的核心逻辑【最复杂的联动逻辑】
|
|
|
|
|
* 核心能力:兼容24/12小时制的小时选择,处理所有小时边界的联动场景,包含「小时→日期」「小时→上下午」的双层联动,同步更新核心日历对象
|
|
|
|
|
* 核心处理场景:
|
|
|
|
|
* 1. 12小时制:下午11点→12点 → 日期+1;上午12点→11点 → 日期-1;小时11↔12时自动切换上下午状态;
|
|
|
|
|
* 2. 24小时制:23点→0点 → 日期+1;0点→23点 → 日期-1;无上下切换逻辑;
|
|
|
|
|
* 3. 所有场景下,最终将选中的小时数适配转换为24小时制,同步至核心日历对象,保证数据统一。
|
|
|
|
|
*/
|
|
|
|
|
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
|
|
|
|
|
@Override
|
|
|
|
|
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
|
|
|
|
|
boolean isDateChanged = false; // 日期是否变化标记
|
|
|
|
|
Calendar cal = Calendar.getInstance();
|
|
|
|
|
boolean isDateChanged = false; // 日期是否发生变化的标记位,默认无变化
|
|
|
|
|
Calendar cal = Calendar.getInstance(); // 临时日历对象,用于处理日期偏移
|
|
|
|
|
|
|
|
|
|
// 12小时制下的小时边界处理
|
|
|
|
|
// ========== 12小时制 小时边界特殊处理 ==========
|
|
|
|
|
if (!mIs24HourView) {
|
|
|
|
|
// 场景1:下午11点→12点 → 日期+1
|
|
|
|
|
// 场景1:下午状态下,小时从11→12,触发日期+1(跨天)
|
|
|
|
|
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
|
|
|
|
|
cal.setTimeInMillis(mDate.getTimeInMillis());
|
|
|
|
|
cal.add(Calendar.DAY_OF_YEAR, 1);
|
|
|
|
|
isDateChanged = true;
|
|
|
|
|
}
|
|
|
|
|
// 场景2:上午12点→11点 → 日期-1
|
|
|
|
|
// 场景2:上午状态下,小时从12→11,触发日期-1(跨天)
|
|
|
|
|
else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
|
|
|
|
|
cal.setTimeInMillis(mDate.getTimeInMillis());
|
|
|
|
|
cal.add(Calendar.DAY_OF_YEAR, -1);
|
|
|
|
|
isDateChanged = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 小时11→12 或 12→11 时,切换上午/下午标记
|
|
|
|
|
// 场景3:小时在11和12之间切换时,自动翻转上下午状态(核心联动逻辑)
|
|
|
|
|
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
|
|
|
|
|
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
|
|
|
|
|
mIsAm = !mIsAm;
|
|
|
|
|
updateAmPmControl(); // 更新上午/下午选择器
|
|
|
|
|
updateAmPmControl(); // 同步刷新上下午选择器的选中状态
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 24小时制下的小时边界处理
|
|
|
|
|
// ========== 24小时制 小时边界特殊处理 ==========
|
|
|
|
|
else {
|
|
|
|
|
// 场景1:23点→0点 → 日期+1
|
|
|
|
|
// 场景1:小时从23→0,触发日期+1(跨天,一天的结束)
|
|
|
|
|
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
|
|
|
|
|
cal.setTimeInMillis(mDate.getTimeInMillis());
|
|
|
|
|
cal.add(Calendar.DAY_OF_YEAR, 1);
|
|
|
|
|
isDateChanged = true;
|
|
|
|
|
}
|
|
|
|
|
// 场景2:0点→23点 → 日期-1
|
|
|
|
|
// 场景2:小时从0→23,触发日期-1(跨天,一天的开始)
|
|
|
|
|
else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
|
|
|
|
|
cal.setTimeInMillis(mDate.getTimeInMillis());
|
|
|
|
|
cal.add(Calendar.DAY_OF_YEAR, -1);
|
|
|
|
|
@ -172,13 +191,13 @@ public class DateTimePicker extends FrameLayout {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算并设置最终的小时数(适配12/24小时制)
|
|
|
|
|
// ========== 统一处理:将选中的小时数转换为24小时制,同步至核心日历对象 ==========
|
|
|
|
|
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
|
|
|
|
|
mDate.set(Calendar.HOUR_OF_DAY, newHour);
|
|
|
|
|
// 触发时间变化回调
|
|
|
|
|
// 触发全局时间变化回调,向外通知小时已更新
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
|
|
|
|
|
// 日期变化时,更新日历的年/月/日
|
|
|
|
|
// ========== 日期发生变化时,同步更新核心日历对象的年月日 ==========
|
|
|
|
|
if (isDateChanged) {
|
|
|
|
|
setCurrentYear(cal.get(Calendar.YEAR));
|
|
|
|
|
setCurrentMonth(cal.get(Calendar.MONTH));
|
|
|
|
|
@ -187,218 +206,232 @@ public class DateTimePicker extends FrameLayout {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 分钟选择器值变化监听器:处理分钟切换,包含边界联动(分钟→小时→日期) */
|
|
|
|
|
/**
|
|
|
|
|
* 分钟选择器 值变化监听器:处理分钟滑动切换的核心逻辑【分钟→小时→日期 三层联动】
|
|
|
|
|
* 核心能力:监听分钟选择器的数值变化,处理分钟的边界联动场景,是最基础也是最核心的时间联动逻辑
|
|
|
|
|
* 核心处理场景:分钟从59→0 触发小时+1;分钟从0→59 触发小时-1;小时变化后可能触发日期变化,自动联动处理
|
|
|
|
|
* 附加能力:小时变化后,自动同步更新上下午状态与选择器,保证12小时制的展示一致性
|
|
|
|
|
*/
|
|
|
|
|
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
|
|
|
|
|
@Override
|
|
|
|
|
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
|
|
|
|
|
int minValue = mMinuteSpinner.getMinValue();
|
|
|
|
|
int maxValue = mMinuteSpinner.getMaxValue();
|
|
|
|
|
int offset = 0; // 小时偏移量
|
|
|
|
|
int offset = 0; // 小时偏移量,默认无偏移
|
|
|
|
|
|
|
|
|
|
// 场景1:59分→0分 → 小时+1
|
|
|
|
|
// 场景1:分钟从59→0,触发小时+1(分钟到顶,进位到小时)
|
|
|
|
|
if (oldVal == maxValue && newVal == minValue) {
|
|
|
|
|
offset += 1;
|
|
|
|
|
}
|
|
|
|
|
// 场景2:0分→59分 → 小时-1
|
|
|
|
|
// 场景2:分钟从0→59,触发小时-1(分钟到底,退位到小时)
|
|
|
|
|
else if (oldVal == minValue && newVal == maxValue) {
|
|
|
|
|
offset -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 小时需要偏移时,调整日历并更新相关选择器
|
|
|
|
|
// ========== 小时需要偏移时,执行联动逻辑 ==========
|
|
|
|
|
if (offset != 0) {
|
|
|
|
|
mDate.add(Calendar.HOUR_OF_DAY, offset); // 调整小时
|
|
|
|
|
mHourSpinner.setValue(getCurrentHour()); // 更新小时选择器
|
|
|
|
|
updateDateControl(); // 更新日期选择器(小时偏移可能导致日期变化)
|
|
|
|
|
mDate.add(Calendar.HOUR_OF_DAY, offset); // 同步更新核心日历对象的小时数
|
|
|
|
|
mHourSpinner.setValue(getCurrentHour()); // 刷新小时选择器的选中值
|
|
|
|
|
updateDateControl(); // 刷新日期选择器,小时偏移可能导致日期变化
|
|
|
|
|
|
|
|
|
|
// 根据新小时数更新上午/下午标记
|
|
|
|
|
// 根据新的小时数,更新上下午状态标记
|
|
|
|
|
int newHour = getCurrentHourOfDay();
|
|
|
|
|
if (newHour >= HOURS_IN_HALF_DAY) {
|
|
|
|
|
mIsAm = false;
|
|
|
|
|
} else {
|
|
|
|
|
mIsAm = true;
|
|
|
|
|
}
|
|
|
|
|
updateAmPmControl(); // 更新上午/下午选择器
|
|
|
|
|
mIsAm = newHour < HOURS_IN_HALF_DAY;
|
|
|
|
|
updateAmPmControl(); // 同步刷新上下午选择器的选中状态
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置最终的分钟数
|
|
|
|
|
// ========== 统一处理:将选中的分钟数同步至核心日历对象 ==========
|
|
|
|
|
mDate.set(Calendar.MINUTE, newVal);
|
|
|
|
|
// 触发时间变化回调
|
|
|
|
|
// 触发全局时间变化回调,向外通知分钟已更新
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 上午/下午选择器值变化监听器:切换上午/下午,调整小时数(±12小时) */
|
|
|
|
|
/**
|
|
|
|
|
* 上下午选择器 值变化监听器:处理上下午切换的核心逻辑【仅12小时制生效】
|
|
|
|
|
* 核心能力:监听上下午选择器的切换操作,翻转上下午状态标记,同步调整核心日历对象的小时数(±12小时)
|
|
|
|
|
* 核心逻辑:上午→下午,小时+12;下午→上午,小时-12,保证时间数值的正确性,无数据误差
|
|
|
|
|
*/
|
|
|
|
|
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
|
|
|
|
|
@Override
|
|
|
|
|
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
|
|
|
|
|
// 切换上午/下午标记
|
|
|
|
|
// 翻转上下午状态标记
|
|
|
|
|
mIsAm = !mIsAm;
|
|
|
|
|
// 调整小时数:上午→下午 +12小时,下午→上午 -12小时
|
|
|
|
|
// 根据新状态,调整核心日历对象的小时数,保证时间正确
|
|
|
|
|
if (mIsAm) {
|
|
|
|
|
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
|
|
|
|
|
} else {
|
|
|
|
|
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
|
|
|
|
|
}
|
|
|
|
|
// 更新上午/下午选择器展示
|
|
|
|
|
// 刷新上下午选择器的展示状态
|
|
|
|
|
updateAmPmControl();
|
|
|
|
|
// 触发时间变化回调
|
|
|
|
|
// 触发全局时间变化回调,向外通知上下午已切换
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ======================== 回调接口 - 日期时间变化通知 ========================
|
|
|
|
|
// ======================== 内部回调接口 - 日期时间变化通知【标准化通信协议】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 日期时间变化回调接口
|
|
|
|
|
* 由外部(如DateTimePickerDialog)实现,接收控件选中的最新日期时间
|
|
|
|
|
* 日期时间变化回调接口:控件对外提供的唯一通信接口,所有时间选择操作的最终数据出口
|
|
|
|
|
* 设计原则:解耦控件内部逻辑与外部业务逻辑,控件只负责提供时间选择能力,不处理任何业务逻辑
|
|
|
|
|
* 调用时机:日期、小时、分钟、上下午任一选择器发生变化时,均会触发该接口的回调方法,实时传递最新的时间数据
|
|
|
|
|
*/
|
|
|
|
|
public interface OnDateTimeChangedListener {
|
|
|
|
|
/**
|
|
|
|
|
* 日期时间变化回调方法
|
|
|
|
|
* @param view 当前的DateTimePicker控件实例
|
|
|
|
|
* @param year 选中的年份
|
|
|
|
|
* @param month 选中的月份(Calendar.MONTH,0=1月,11=12月)
|
|
|
|
|
* @param dayOfMonth 选中的日期(当月的第几天)
|
|
|
|
|
* @param hourOfDay 选中的小时(24小时制,0~23)
|
|
|
|
|
* @param minute 选中的分钟(0~59)
|
|
|
|
|
* 日期时间变化的回调方法,传递标准化的时间数据
|
|
|
|
|
* @param view 当前的DateTimePicker控件实例,外部可通过该实例获取更多信息或执行操作
|
|
|
|
|
* @param year 选中的年份,如 2026
|
|
|
|
|
* @param month 选中的月份,遵循Calendar规范,0代表1月,11代表12月
|
|
|
|
|
* @param dayOfMonth 选中的日期,当月的第几天,如 15
|
|
|
|
|
* @param hourOfDay 选中的小时,固定为24小时制数值,0~23,外部无需做格式转换,直接使用
|
|
|
|
|
* @param minute 选中的分钟,0~59,标准化数值
|
|
|
|
|
*/
|
|
|
|
|
void onDateTimeChanged(DateTimePicker view, int year, int month,
|
|
|
|
|
int dayOfMonth, int hourOfDay, int minute);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 构造方法 ========================
|
|
|
|
|
// ======================== 构造方法区 - 三级重载构造,满足不同初始化需求【核心初始化入口】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 构造方法1:默认初始化(使用当前系统时间,适配系统24小时制)
|
|
|
|
|
* @param context 应用上下文
|
|
|
|
|
* 构造方法1:最简初始化,无参重载
|
|
|
|
|
* 核心能力:使用系统当前时间作为初始值,自动适配系统的24小时制设置,一键创建控件
|
|
|
|
|
* @param context 应用上下文对象,不可为空
|
|
|
|
|
*/
|
|
|
|
|
public DateTimePicker(Context context) {
|
|
|
|
|
this(context, System.currentTimeMillis());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构造方法2:指定初始时间,适配系统24小时制
|
|
|
|
|
* @param context 应用上下文
|
|
|
|
|
* @param date 初始时间戳(毫秒级)
|
|
|
|
|
* 构造方法2:指定初始时间初始化
|
|
|
|
|
* 核心能力:传入指定的毫秒级时间戳作为初始值,自动适配系统的24小时制设置,灵活适配业务需求
|
|
|
|
|
* @param context 应用上下文对象,不可为空
|
|
|
|
|
* @param date 初始选中的时间戳,单位:毫秒,支持任意合法的时间戳
|
|
|
|
|
*/
|
|
|
|
|
public DateTimePicker(Context context, long date) {
|
|
|
|
|
this(context, date, DateFormat.is24HourFormat(context));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构造方法3:指定初始时间和24小时制状态,核心初始化逻辑
|
|
|
|
|
* @param context 应用上下文
|
|
|
|
|
* @param date 初始时间戳(毫秒级)
|
|
|
|
|
* @param is24HourView 是否使用24小时制
|
|
|
|
|
* 构造方法3:全参数核心初始化,控件的最终初始化入口,所有构造方法最终均调用此方法
|
|
|
|
|
* 核心能力:一站式完成「上下文初始化+核心数据初始化+布局填充+子选择器初始化+监听器绑定+状态配置+初始时间设置」,无需外部执行任何额外配置,开箱即用
|
|
|
|
|
* @param context 应用上下文对象,用于加载布局、创建控件、获取系统配置
|
|
|
|
|
* @param date 初始选中的时间戳,单位:毫秒,控件打开时默认展示的时间
|
|
|
|
|
* @param is24HourView 是否启用24小时制,true=启用,false=启用12小时制
|
|
|
|
|
*/
|
|
|
|
|
public DateTimePicker(Context context, long date, boolean is24HourView) {
|
|
|
|
|
super(context);
|
|
|
|
|
mDate = Calendar.getInstance(); // 初始化核心日历对象
|
|
|
|
|
mInitialising = true; // 标记进入初始化阶段(避免触发不必要的回调)
|
|
|
|
|
// 初始化上午/下午标记:根据当前小时数判断(≥12为下午)
|
|
|
|
|
mDate = Calendar.getInstance(); // 初始化核心日历对象,默认加载系统当前时间
|
|
|
|
|
mInitialising = true; // 标记进入初始化阶段,屏蔽所有回调触发,防止无效数据通知
|
|
|
|
|
// 初始化上下午状态标记:根据当前小时数判断,≥12为下午,<12为上午
|
|
|
|
|
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
|
|
|
|
|
|
|
|
|
|
// 加载控件布局(datetime_picker.xml)
|
|
|
|
|
// 第一步:填充控件的核心布局,将xml布局文件加载至当前FrameLayout根容器
|
|
|
|
|
inflate(context, R.layout.datetime_picker, this);
|
|
|
|
|
|
|
|
|
|
// 初始化日期选择器
|
|
|
|
|
// 第二步:初始化所有子滚轮选择器,绑定控件ID,配置基础属性与监听器
|
|
|
|
|
// 初始化日期选择器:配置取值范围+绑定值变化监听器
|
|
|
|
|
mDateSpinner = (NumberPicker) findViewById(R.id.date);
|
|
|
|
|
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
|
|
|
|
|
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
|
|
|
|
|
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
|
|
|
|
|
|
|
|
|
|
// 初始化小时选择器
|
|
|
|
|
// 初始化小时选择器:仅绑定值变化监听器,取值范围在24/12小时制设置时动态配置
|
|
|
|
|
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
|
|
|
|
|
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
|
|
|
|
|
|
|
|
|
|
// 初始化分钟选择器
|
|
|
|
|
// 初始化分钟选择器:配置取值范围+设置长按快速滚动间隔+绑定值变化监听器,细节体验优化
|
|
|
|
|
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
|
|
|
|
|
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
|
|
|
|
|
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
|
|
|
|
|
mMinuteSpinner.setLongPressUpdateInterval(100); // 长按快速调整的间隔(100ms)
|
|
|
|
|
mMinuteSpinner.setLongPressUpdateInterval(100); // 长按滚动间隔100ms,滚动更快更流畅
|
|
|
|
|
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
|
|
|
|
|
|
|
|
|
|
// 初始化上午/下午选择器
|
|
|
|
|
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); // 获取系统AM/PM文本(适配多语言)
|
|
|
|
|
// 初始化上下午选择器:配置取值范围+加载系统原生的AM/PM文本(适配多语言)+绑定值变化监听器
|
|
|
|
|
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
|
|
|
|
|
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
|
|
|
|
|
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
|
|
|
|
|
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
|
|
|
|
|
mAmPmSpinner.setDisplayedValues(stringsForAmPm); // 设置AM/PM展示文本
|
|
|
|
|
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
|
|
|
|
|
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
|
|
|
|
|
|
|
|
|
|
// 更新控件到初始状态
|
|
|
|
|
// 第三步:刷新所有子选择器的初始展示状态,保证UI与初始数据一致
|
|
|
|
|
updateDateControl();
|
|
|
|
|
updateHourControl();
|
|
|
|
|
updateAmPmControl();
|
|
|
|
|
|
|
|
|
|
// 设置24小时制状态
|
|
|
|
|
// 第四步:配置24/12小时制状态,完成制式适配
|
|
|
|
|
set24HourView(is24HourView);
|
|
|
|
|
|
|
|
|
|
// 设置初始时间
|
|
|
|
|
// 第五步:设置控件的初始选中时间,同步至所有选择器与核心日历对象
|
|
|
|
|
setCurrentDate(date);
|
|
|
|
|
|
|
|
|
|
// 设置控件启用状态
|
|
|
|
|
// 第六步:配置控件的整体启用状态,默认启用
|
|
|
|
|
setEnabled(isEnabled());
|
|
|
|
|
|
|
|
|
|
// 初始化完成,取消标记
|
|
|
|
|
// 第七步:初始化完成,解除初始化标记,控件进入正常工作状态,可响应所有操作与回调
|
|
|
|
|
mInitialising = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 重写方法 - 控件启用/禁用 ========================
|
|
|
|
|
// ======================== 重写父类方法 - 控件整体启用/禁用【统一状态管控】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 设置控件整体启用/禁用状态
|
|
|
|
|
* @param enabled true=启用(所有选择器可交互),false=禁用(所有选择器不可交互)
|
|
|
|
|
* 重写FrameLayout的setEnabled方法,实现控件整体的启用/禁用控制
|
|
|
|
|
* 核心能力:一键同步所有子滚轮选择器的交互状态,无需单独配置每个选择器,保证状态一致性
|
|
|
|
|
* 优化点:状态未变化时直接返回,避免重复执行无效操作,提升性能
|
|
|
|
|
* @param enabled true=启用,所有选择器可滑动选择;false=禁用,所有选择器不可交互,灰显
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public void setEnabled(boolean enabled) {
|
|
|
|
|
if (mIsEnabled == enabled) {
|
|
|
|
|
return; // 状态未变化,直接返回
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
super.setEnabled(enabled);
|
|
|
|
|
// 同步所有选择器的启用状态
|
|
|
|
|
// 同步所有子选择器的启用状态
|
|
|
|
|
mDateSpinner.setEnabled(enabled);
|
|
|
|
|
mMinuteSpinner.setEnabled(enabled);
|
|
|
|
|
mHourSpinner.setEnabled(enabled);
|
|
|
|
|
mAmPmSpinner.setEnabled(enabled);
|
|
|
|
|
// 更新启用状态标记
|
|
|
|
|
// 更新全局启用状态标记
|
|
|
|
|
mIsEnabled = enabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取控件整体启用状态
|
|
|
|
|
* @return true=启用,false=禁用
|
|
|
|
|
* 重写FrameLayout的isEnabled方法,获取控件的整体启用状态
|
|
|
|
|
* @return true=控件已启用,false=控件已禁用
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public boolean isEnabled() {
|
|
|
|
|
return mIsEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 公共方法 - 日期时间获取/设置 ========================
|
|
|
|
|
// ======================== 公共方法区 - 日期时间 取值/赋值 标准化API【外部核心调用接口,最全】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前选中的日期时间戳(毫秒级)
|
|
|
|
|
* @return 选中时间的毫秒数
|
|
|
|
|
* 获取当前选中的完整时间戳,外部最常用的取值方法
|
|
|
|
|
* @return 选中时间的毫秒级时间戳,可直接用于存储、传输、转换,标准化输出
|
|
|
|
|
*/
|
|
|
|
|
public long getCurrentDateInTimeMillis() {
|
|
|
|
|
return mDate.getTimeInMillis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置当前选中的日期时间(通过时间戳)
|
|
|
|
|
* @param date 要设置的时间戳(毫秒级)
|
|
|
|
|
* 设置当前选中的时间,通过毫秒级时间戳赋值,外部最常用的赋值方法
|
|
|
|
|
* 核心能力:自动解析时间戳为年月日时分,同步至核心日历对象与所有子选择器,无需手动拆分
|
|
|
|
|
* @param date 要设置的时间戳,单位:毫秒,支持任意合法的时间戳
|
|
|
|
|
*/
|
|
|
|
|
public void setCurrentDate(long date) {
|
|
|
|
|
Calendar cal = Calendar.getInstance();
|
|
|
|
|
cal.setTimeInMillis(date);
|
|
|
|
|
// 解析时间戳为年/月/日/时/分,调用重载方法设置
|
|
|
|
|
// 解析时间戳为标准化的年月日时分,调用重载方法完成赋值
|
|
|
|
|
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
|
|
|
|
|
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置当前选中的日期时间(通过年/月/日/时/分)
|
|
|
|
|
* @param year 年份
|
|
|
|
|
* @param month 月份(Calendar.MONTH,0=1月)
|
|
|
|
|
* @param dayOfMonth 日期(当月的第几天)
|
|
|
|
|
* @param hourOfDay 小时(24小时制,0~23)
|
|
|
|
|
* @param minute 分钟(0~59)
|
|
|
|
|
* 设置当前选中的时间,通过标准化的年月日时分赋值,最底层的赋值方法
|
|
|
|
|
* 核心能力:精细化控制每个时间维度的数值,同步至核心日历对象与所有子选择器,数据完全可控
|
|
|
|
|
* @param year 年份,如 2026
|
|
|
|
|
* @param month 月份,Calendar规范,0=1月
|
|
|
|
|
* @param dayOfMonth 日期,当月的第几天,1~31
|
|
|
|
|
* @param hourOfDay 小时,24小时制,0~23
|
|
|
|
|
* @param minute 分钟,0~59
|
|
|
|
|
*/
|
|
|
|
|
public void setCurrentDate(int year, int month,
|
|
|
|
|
int dayOfMonth, int hourOfDay, int minute) {
|
|
|
|
|
@ -411,7 +444,7 @@ public class DateTimePicker extends FrameLayout {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前选中的年份
|
|
|
|
|
* @return 年份(如2025)
|
|
|
|
|
* @return 年份数值,如 2026
|
|
|
|
|
*/
|
|
|
|
|
public int getCurrentYear() {
|
|
|
|
|
return mDate.get(Calendar.YEAR);
|
|
|
|
|
@ -419,21 +452,21 @@ public class DateTimePicker extends FrameLayout {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置当前选中的年份
|
|
|
|
|
* @param year 要设置的年份(如2025)
|
|
|
|
|
* 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
|
|
|
|
|
* @param year 要设置的年份,如 2026
|
|
|
|
|
*/
|
|
|
|
|
public void setCurrentYear(int year) {
|
|
|
|
|
// 初始化中或年份未变化时,直接返回
|
|
|
|
|
if (!mInitialising && year == getCurrentYear()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mDate.set(Calendar.YEAR, year);
|
|
|
|
|
updateDateControl(); // 更新日期选择器展示
|
|
|
|
|
onDateTimeChanged(); // 触发时间变化回调
|
|
|
|
|
updateDateControl();
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前选中的月份
|
|
|
|
|
* @return 月份(Calendar.MONTH,0=1月,11=12月)
|
|
|
|
|
* @return 月份数值,遵循Calendar规范,0代表1月,11代表12月
|
|
|
|
|
*/
|
|
|
|
|
public int getCurrentMonth() {
|
|
|
|
|
return mDate.get(Calendar.MONTH);
|
|
|
|
|
@ -441,216 +474,212 @@ public class DateTimePicker extends FrameLayout {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置当前选中的月份
|
|
|
|
|
* @param month 要设置的月份(Calendar.MONTH,0=1月)
|
|
|
|
|
* 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
|
|
|
|
|
* @param month 要设置的月份,Calendar规范,0=1月
|
|
|
|
|
*/
|
|
|
|
|
public void setCurrentMonth(int month) {
|
|
|
|
|
// 初始化中或月份未变化时,直接返回
|
|
|
|
|
if (!mInitialising && month == getCurrentMonth()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mDate.set(Calendar.MONTH, month);
|
|
|
|
|
updateDateControl(); // 更新日期选择器展示
|
|
|
|
|
onDateTimeChanged(); // 触发时间变化回调
|
|
|
|
|
updateDateControl();
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前选中的日期(当月的第几天)
|
|
|
|
|
* @return 日期(1~31)
|
|
|
|
|
* 获取当前选中的日期
|
|
|
|
|
* @return 日期数值,当月的第几天,1~31
|
|
|
|
|
*/
|
|
|
|
|
public int getCurrentDay() {
|
|
|
|
|
return mDate.get(Calendar.DAY_OF_MONTH);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置当前选中的日期(当月的第几天)
|
|
|
|
|
* @param dayOfMonth 要设置的日期(1~31)
|
|
|
|
|
* 设置当前选中的日期
|
|
|
|
|
* 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
|
|
|
|
|
* @param dayOfMonth 要设置的日期,1~31
|
|
|
|
|
*/
|
|
|
|
|
public void setCurrentDay(int dayOfMonth) {
|
|
|
|
|
// 初始化中或日期未变化时,直接返回
|
|
|
|
|
if (!mInitialising && dayOfMonth == getCurrentDay()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
|
|
|
|
|
updateDateControl(); // 更新日期选择器展示
|
|
|
|
|
onDateTimeChanged(); // 触发时间变化回调
|
|
|
|
|
updateDateControl();
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前选中的小时(24小时制,0~23)
|
|
|
|
|
* @return 小时数(0~23)
|
|
|
|
|
* 获取当前选中的小时数,固定返回24小时制数值,标准化输出,外部无需转换
|
|
|
|
|
* @return 小时数值,0~23
|
|
|
|
|
*/
|
|
|
|
|
public int getCurrentHourOfDay() {
|
|
|
|
|
return mDate.get(Calendar.HOUR_OF_DAY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 内部方法:获取适配当前制式的小时数(24/12小时制)
|
|
|
|
|
* @return 24小时制返回0~23,12小时制返回1~12
|
|
|
|
|
* 内部私有方法:获取适配当前制式的小时数
|
|
|
|
|
* 核心能力:根据24/12小时制,返回对应范围的小时数,供内部选择器赋值使用,不对外暴露
|
|
|
|
|
* @return 24小时制返回0~23,12小时制返回1~12,适配选择器的取值范围
|
|
|
|
|
*/
|
|
|
|
|
private int getCurrentHour() {
|
|
|
|
|
if (mIs24HourView){
|
|
|
|
|
return getCurrentHourOfDay(); // 24小时制直接返回
|
|
|
|
|
return getCurrentHourOfDay();
|
|
|
|
|
} else {
|
|
|
|
|
// 12小时制转换:0点→12点,13点→1点,依此类推
|
|
|
|
|
int hour = getCurrentHourOfDay();
|
|
|
|
|
if (hour > HOURS_IN_HALF_DAY) {
|
|
|
|
|
return hour - HOURS_IN_HALF_DAY;
|
|
|
|
|
} else {
|
|
|
|
|
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
|
|
|
|
|
}
|
|
|
|
|
// 12小时制特殊转换:0点→12点,13点→1点,保证选择器展示正确
|
|
|
|
|
return hour > HOURS_IN_HALF_DAY ? hour - HOURS_IN_HALF_DAY : (hour == 0 ? HOURS_IN_HALF_DAY : hour);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置当前选中的小时(24小时制,0~23)
|
|
|
|
|
* @param hourOfDay 要设置的小时数(0~23)
|
|
|
|
|
* 设置当前选中的小时数,接收24小时制数值,标准化输入,内部自动适配转换
|
|
|
|
|
* 核心能力:自动处理24/12小时制的转换逻辑,同步更新上下午状态与选择器,数据无误差
|
|
|
|
|
* 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
|
|
|
|
|
* @param hourOfDay 要设置的小时数,24小时制,0~23
|
|
|
|
|
*/
|
|
|
|
|
public void setCurrentHour(int hourOfDay) {
|
|
|
|
|
// 初始化中或小时未变化时,直接返回
|
|
|
|
|
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
|
|
|
|
|
|
|
|
|
|
// 12小时制下,更新上午/下午标记和选择器
|
|
|
|
|
// 12小时制下的特殊处理:转换小时数+更新上下午状态
|
|
|
|
|
if (!mIs24HourView) {
|
|
|
|
|
if (hourOfDay >= HOURS_IN_HALF_DAY) {
|
|
|
|
|
mIsAm = false; // ≥12为下午
|
|
|
|
|
if (hourOfDay > HOURS_IN_HALF_DAY) {
|
|
|
|
|
hourOfDay -= HOURS_IN_HALF_DAY; // 转换为12小时制(13→1,14→2...)
|
|
|
|
|
}
|
|
|
|
|
mIsAm = false;
|
|
|
|
|
hourOfDay = hourOfDay > HOURS_IN_HALF_DAY ? hourOfDay - HOURS_IN_HALF_DAY : hourOfDay;
|
|
|
|
|
} else {
|
|
|
|
|
mIsAm = true; // <12为上午
|
|
|
|
|
if (hourOfDay == 0) {
|
|
|
|
|
hourOfDay = HOURS_IN_HALF_DAY; // 0点→12点
|
|
|
|
|
}
|
|
|
|
|
mIsAm = true;
|
|
|
|
|
hourOfDay = hourOfDay == 0 ? HOURS_IN_HALF_DAY : hourOfDay;
|
|
|
|
|
}
|
|
|
|
|
updateAmPmControl(); // 更新上午/下午选择器
|
|
|
|
|
updateAmPmControl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置小时选择器的值
|
|
|
|
|
mHourSpinner.setValue(hourOfDay);
|
|
|
|
|
// 触发时间变化回调
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前选中的分钟
|
|
|
|
|
* @return 分钟数(0~59)
|
|
|
|
|
* 获取当前选中的分钟数
|
|
|
|
|
* @return 分钟数值,0~59
|
|
|
|
|
*/
|
|
|
|
|
public int getCurrentMinute() {
|
|
|
|
|
return mDate.get(Calendar.MINUTE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置当前选中的分钟
|
|
|
|
|
* @param minute 要设置的分钟数(0~59)
|
|
|
|
|
* 设置当前选中的分钟数
|
|
|
|
|
* 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
|
|
|
|
|
* @param minute 要设置的分钟数,0~59
|
|
|
|
|
*/
|
|
|
|
|
public void setCurrentMinute(int minute) {
|
|
|
|
|
// 初始化中或分钟未变化时,直接返回
|
|
|
|
|
if (!mInitialising && minute == getCurrentMinute()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mMinuteSpinner.setValue(minute); // 设置分钟选择器的值
|
|
|
|
|
mDate.set(Calendar.MINUTE, minute); // 更新日历对象
|
|
|
|
|
onDateTimeChanged(); // 触发时间变化回调
|
|
|
|
|
mMinuteSpinner.setValue(minute);
|
|
|
|
|
mDate.set(Calendar.MINUTE, minute);
|
|
|
|
|
onDateTimeChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 公共方法 - 24小时制适配 ========================
|
|
|
|
|
// ======================== 公共方法区 - 24小时制适配【系统级适配能力】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 判断当前是否为24小时制
|
|
|
|
|
* 判断当前控件是否启用24小时制
|
|
|
|
|
* @return true=24小时制,false=12小时制
|
|
|
|
|
*/
|
|
|
|
|
public boolean is24HourView () {
|
|
|
|
|
return mIs24HourView;
|
|
|
|
|
}
|
|
|
|
|
public boolean is24HourView () {
|
|
|
|
|
return mIs24HourView;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置24小时制/12小时制
|
|
|
|
|
* @param is24HourView true=24小时制(隐藏AM/PM选择器),false=12小时制(显示AM/PM选择器)
|
|
|
|
|
* 设置控件的时间展示制式,手动强制切换24/12小时制,优先级高于系统配置
|
|
|
|
|
* 核心能力:切换制式时,自动刷新小时选择器的取值范围、上下午选择器的可见性、当前小时数的展示值,无缝切换无感知
|
|
|
|
|
* 优化点:状态未变化时直接返回,避免无效操作
|
|
|
|
|
* @param is24HourView true=启用24小时制,隐藏上下午选择器;false=启用12小时制,显示上下午选择器
|
|
|
|
|
*/
|
|
|
|
|
public void set24HourView(boolean is24HourView) {
|
|
|
|
|
if (mIs24HourView == is24HourView) {
|
|
|
|
|
return; // 状态未变化,直接返回
|
|
|
|
|
}
|
|
|
|
|
mIs24HourView = is24HourView;
|
|
|
|
|
// 显示/隐藏上午/下午选择器
|
|
|
|
|
public void set24HourView(boolean is24HourView) {
|
|
|
|
|
if (mIs24HourView == is24HourView) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mIs24HourView = is24HourView;
|
|
|
|
|
// 控制上下午选择器的可见性
|
|
|
|
|
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
|
|
|
|
|
// 获取当前小时数(用于重置选择器)
|
|
|
|
|
int hour = getCurrentHourOfDay();
|
|
|
|
|
// 更新小时选择器的取值范围
|
|
|
|
|
// 刷新小时选择器的取值范围
|
|
|
|
|
updateHourControl();
|
|
|
|
|
// 重新设置小时数(适配新的制式)
|
|
|
|
|
// 重新适配并设置小时数,保证展示正确
|
|
|
|
|
setCurrentHour(hour);
|
|
|
|
|
// 更新上午/下午选择器状态
|
|
|
|
|
// 刷新上下午选择器的状态
|
|
|
|
|
updateAmPmControl();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 内部方法 - 控件更新 ========================
|
|
|
|
|
// ======================== 内部私有方法区 - 控件UI刷新【核心UI更新逻辑,内部闭环】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 更新日期选择器的展示文本:生成近7天的格式化日期(如“12.23 星期二”)
|
|
|
|
|
* 刷新日期选择器的展示文本与选中状态
|
|
|
|
|
* 核心能力:重新计算并生成近7天的格式化日期文本,更新至日期选择器,保证展示的日期与核心日历对象一致
|
|
|
|
|
* 展示格式:固定为「MM.dd EEEE」,即「月.日 星期」,如「01.15 星期四」
|
|
|
|
|
*/
|
|
|
|
|
private void updateDateControl() {
|
|
|
|
|
Calendar cal = Calendar.getInstance();
|
|
|
|
|
cal.setTimeInMillis(mDate.getTimeInMillis());
|
|
|
|
|
// 计算近7天的起始日期(当前日期 - 4天,确保中间为当前日期)
|
|
|
|
|
// 计算近7天的起始日期:当前日期向前推4天,保证选中日期在列表中间位置,交互更友好
|
|
|
|
|
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
|
|
|
|
|
|
|
|
|
|
mDateSpinner.setDisplayedValues(null); // 清空原有展示文本
|
|
|
|
|
// 生成近7天的格式化日期文本
|
|
|
|
|
mDateSpinner.setDisplayedValues(null); // 清空原有文本,避免残留
|
|
|
|
|
// 循环生成7天的格式化日期文本
|
|
|
|
|
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
|
|
|
|
|
cal.add(Calendar.DAY_OF_YEAR, 1);
|
|
|
|
|
// 格式化:月.日 星期(如“12.23 星期二”)
|
|
|
|
|
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
|
|
|
|
|
}
|
|
|
|
|
// 设置日期选择器的展示文本
|
|
|
|
|
// 将生成的文本数组设置到日期选择器
|
|
|
|
|
mDateSpinner.setDisplayedValues(mDateDisplayValues);
|
|
|
|
|
// 设置默认选中中间项(当前日期)
|
|
|
|
|
// 默认选中列表中间项,即当前日期
|
|
|
|
|
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
|
|
|
|
|
mDateSpinner.invalidate(); // 刷新选择器UI
|
|
|
|
|
mDateSpinner.invalidate(); // 强制刷新UI,保证展示生效
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新上午/下午选择器的状态:设置选中项、控制可见性
|
|
|
|
|
* 刷新上下午选择器的选中状态与可见性
|
|
|
|
|
* 核心能力:根据当前的24小时制状态与上下午标记,同步更新上下午选择器的展示,保证UI与数据一致
|
|
|
|
|
*/
|
|
|
|
|
private void updateAmPmControl() {
|
|
|
|
|
if (mIs24HourView) {
|
|
|
|
|
mAmPmSpinner.setVisibility(View.GONE); // 24小时制隐藏
|
|
|
|
|
mAmPmSpinner.setVisibility(View.GONE);
|
|
|
|
|
} else {
|
|
|
|
|
// 设置选中项:0=AM/上午,1=PM/下午
|
|
|
|
|
// 设置选中项:0=上午/AM,1=下午/PM
|
|
|
|
|
int index = mIsAm ? Calendar.AM : Calendar.PM;
|
|
|
|
|
mAmPmSpinner.setValue(index);
|
|
|
|
|
mAmPmSpinner.setVisibility(View.VISIBLE); // 12小时制显示
|
|
|
|
|
mAmPmSpinner.setVisibility(View.VISIBLE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新小时选择器的取值范围:适配24/12小时制
|
|
|
|
|
* 刷新小时选择器的取值范围
|
|
|
|
|
* 核心能力:根据当前的24小时制状态,动态配置小时选择器的最小值与最大值,适配不同制式的展示需求
|
|
|
|
|
*/
|
|
|
|
|
private void updateHourControl() {
|
|
|
|
|
if (mIs24HourView) {
|
|
|
|
|
// 24小时制:0~23
|
|
|
|
|
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
|
|
|
|
|
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
|
|
|
|
|
} else {
|
|
|
|
|
// 12小时制:1~12
|
|
|
|
|
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
|
|
|
|
|
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 公共方法 - 回调设置 ========================
|
|
|
|
|
private void updateHourControl() {
|
|
|
|
|
if (mIs24HourView) {
|
|
|
|
|
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
|
|
|
|
|
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
|
|
|
|
|
} else {
|
|
|
|
|
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
|
|
|
|
|
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 公共方法区 - 回调监听器绑定【通信入口】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 设置日期时间变化回调监听器
|
|
|
|
|
* @param callback 外部实现的监听器(null则不触发回调)
|
|
|
|
|
* 设置日期时间变化的回调监听器,外部通过此方法绑定业务逻辑
|
|
|
|
|
* @param callback 外部实现的OnDateTimeChangedListener接口实例,传null则取消监听
|
|
|
|
|
*/
|
|
|
|
|
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
|
|
|
|
|
mOnDateTimeChangedListener = callback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================== 内部方法 - 回调触发 ========================
|
|
|
|
|
// ======================== 内部私有方法区 - 回调触发【核心通信逻辑,内部闭环】 ========================
|
|
|
|
|
/**
|
|
|
|
|
* 触发日期时间变化回调:传递最新的年/月/日/时/分
|
|
|
|
|
* 触发日期时间变化的回调方法,所有选择操作的最终数据出口
|
|
|
|
|
* 核心能力:从核心日历对象中读取标准化的年月日时分数据,调用外部绑定的监听器,完成数据传递
|
|
|
|
|
* 优化点:监听器为null时直接返回,避免空指针异常
|
|
|
|
|
*/
|
|
|
|
|
private void onDateTimeChanged() {
|
|
|
|
|
if (mOnDateTimeChangedListener != null) {
|
|
|
|
|
|