/* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.micode.notes.ui; import android.content.Context; import android.os.Build; import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.FrameLayout; import android.widget.NumberPicker; import android.widget.Toast; import net.micode.notes.R; import java.text.DateFormatSymbols; import java.util.Calendar; import java.util.Locale; /** * 日期时间选择器控件 - 提供完整的日期和时间选择功能 * * 功能增强: * 1. 支持日期范围限制 (设置最小/最大日期) * 2. 添加星期名称本地化处理 * 3. 支持日期格式化定制 * 4. 增加日期验证和错误提示 * 5. 改进时间滚动逻辑 * 6. 支持深色模式 * 7. 添加无障碍支持 * * 修复问题: * 1. 修复12/24小时转换逻辑错误 * 2. 解决日期显示越界问题 * 3. 优化AM/PM切换处理 * 4. 改进初始状态标记处理 */ public class DateTimePicker extends FrameLayout { private static final String TAG = "DateTimePicker"; // 配置常量 private static final boolean DEFAULT_ENABLE_STATE = true; private static final int HOURS_IN_HALF_DAY = 12; private static final int HOURS_IN_ALL_DAY = 24; private static final int DAYS_IN_ALL_WEEK = 7; private static final int DATE_SPINNER_MIN_VAL = 0; private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; private static final int MINUTE_SPINNER_MIN_VAL = 0; private static final int MINUTE_SPINNER_MAX_VAL = 59; private static final int AMPM_SPINNER_MIN_VAL = 0; private static final int AMPM_SPINNER_MAX_VAL = 1; // 状态常量 private static final int MODE_INITIALIZING = 0; private static final int MODE_NORMAL = 1; // 范围限制 (默认无限制) private long mMinDate = Long.MIN_VALUE; private long mMaxDate = Long.MAX_VALUE; // UI组件 private final NumberPicker mDateSpinner; private final NumberPicker mHourSpinner; private final NumberPicker mMinuteSpinner; private final NumberPicker mAmPmSpinner; // 数据模型 private Calendar mDate; private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; private boolean mIsAm; private boolean mIs24HourView; private boolean mIsEnabled = DEFAULT_ENABLE_STATE; private int mState = MODE_INITIALIZING; // 监听器 private OnDateTimeChangedListener mOnDateTimeChangedListener; /** * 日期变更监听器 */ private final NumberPicker.OnValueChangeListener mOnDateChangedListener = (picker, oldVal, newVal) -> { // 计算日期变化差值 int dayDiff = newVal - oldVal; mDate.add(Calendar.DAY_OF_YEAR, dayDiff); // 验证日期范围 if (!isDateInRange()) { revertDateChange(); showDateRangeWarning(); return; } updateDateControl(); onDateTimeChanged(); }; /** * 小时变更监听器 */ private final NumberPicker.OnValueChangeListener mOnHourChangedListener = (picker, oldVal, newVal) -> { int newHour = computeNewHour(oldVal, newVal); mDate.set(Calendar.HOUR_OF_DAY, newHour); onDateTimeChanged(); }; /** * 分钟变更监听器 */ private final NumberPicker.OnValueChangeListener mOnMinuteChangedListener = (picker, oldVal, newVal) -> { mDate.set(Calendar.MINUTE, newVal); onDateTimeChanged(); }; /** * AM/PM变更监听器 */ private final NumberPicker.OnValueChangeListener mOnAmPmChangedListener = (picker, oldVal, newVal) -> { // 切换AM/PM状态 mIsAm = (newVal == Calendar.AM); // 调整时间:AM->PM加12小时,PM->AM减12小时 int hour = mDate.get(Calendar.HOUR_OF_DAY); hour = mIsAm ? hour % 12 : hour % 12 + 12; mDate.set(Calendar.HOUR_OF_DAY, hour); updateHourControl(); onDateTimeChanged(); }; /** * 日期时间变更回调接口 */ public interface OnDateTimeChangedListener { void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute); } /*************************** 构造函数 ***************************/ public DateTimePicker(Context context) { this(context, null); } public DateTimePicker(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DateTimePicker(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 使用当前时间初始化 init(context, System.currentTimeMillis(), DateFormat.is24HourFormat(context)); } public DateTimePicker(Context context, long date) { this(context, date, DateFormat.is24HourFormat(context)); } public DateTimePicker(Context context, long date, boolean is24HourView) { super(context); init(context, date, is24HourView); } /** * 初始化日期时间选择器 */ private void init(Context context, long date, boolean is24HourView) { // 初始化日期对象 mDate = Calendar.getInstance(); mState = MODE_INITIALIZING; mIs24HourView = is24HourView; // 确定初始AM/PM状态 int currentHour = (int) (date / (60 * 60 * 1000) % 24); mIsAm = currentHour < 12; // 加载布局 inflate(context, R.layout.datetime_picker, this); // 初始化日期选择器 mDateSpinner = findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); mDateSpinner.setWrapSelectorWheel(false); // 初始化小时选择器 mHourSpinner = findViewById(R.id.hour); mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); // 初始化分钟选择器 mMinuteSpinner = findViewById(R.id.minute); mMinuteSpinner.setMinValue(MINUTE_SPINNER_MIN_VAL); mMinuteSpinner.setMaxValue(MINUTE_SPINNER_MAX_VAL); mMinuteSpinner.setOnLongPressUpdateInterval(100); mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); // 初始化AM/PM选择器 mAmPmSpinner = findViewById(R.id.amPm); mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); // 本地化AM/PM符号 updateAmPmSymbols(); mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); // 更新UI控件 updateDateControl(); updateHourControl(); updateAmPmControl(); // 设置初始时间 setCurrentDate(date); // 应用启用状态 setEnabled(isEnabled()); // 应用深色模式 applyDarkMode(); // 初始化完成 mState = MODE_NORMAL; } /*************************** 公共方法 ***************************/ @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { return; } super.setEnabled(enabled); // 设置子控件状态 mDateSpinner.setEnabled(enabled); mMinuteSpinner.setEnabled(enabled); mHourSpinner.setEnabled(enabled); mAmPmSpinner.setEnabled(enabled); mIsEnabled = enabled; } @Override public boolean isEnabled() { return mIsEnabled; } /** * 获取当前时间戳 (毫秒) */ public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } /** * 设置当前时间 */ public void setCurrentDate(long date) { // 验证日期范围 if (date < mMinDate || date > mMaxDate) { Log.w(TAG, "设置日期超出范围: " + date); return; } 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) ); } /** * 设置日期时间 */ public void setCurrentDate(int year, int month, int dayOfMonth, int hourOfDay, int minute) { setCurrentYear(year); setCurrentMonth(month); setCurrentDay(dayOfMonth); setCurrentHour(hourOfDay); setCurrentMinute(minute); } /** * 获取当前年份 */ public int getCurrentYear() { return mDate.get(Calendar.YEAR); } /** * 设置当前年份 */ public void setCurrentYear(int year) { // 检查状态避免不必要的更新 if (mState != MODE_INITIALIZING && year == getCurrentYear()) { return; } mDate.set(Calendar.YEAR, year); updateDateControl(); onDateTimeChanged(); } /** * 获取当前月份 */ public int getCurrentMonth() { return mDate.get(Calendar.MONTH); } /** * 设置当前月份 */ public void setCurrentMonth(int month) { if (mState != MODE_INITIALIZING && month == getCurrentMonth()) { return; } mDate.set(Calendar.MONTH, month); updateDateControl(); onDateTimeChanged(); } /** * 获取当前日期 */ public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); } /** * 设置当前日期 */ public void setCurrentDay(int dayOfMonth) { if (mState != MODE_INITIALIZING && dayOfMonth == getCurrentDay()) { return; } mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); updateDateControl(); onDateTimeChanged(); } /** * 获取当前小时(24小时制) */ public int getCurrentHourOfDay() { return mDate.get(Calendar.HOUR_OF_DAY); } /** * 根据显示模式获取小时 */ private int getCurrentDisplayHour() { int hour = getCurrentHourOfDay(); if (mIs24HourView) { return hour; } else { if (hour == 0 || hour == 12) { return 12; } return hour % 12; } } /** * 设置当前小时(24小时制) */ public void setCurrentHour(int hourOfDay) { // 验证小时值是否有效 if (hourOfDay < 0 || hourOfDay > 23) { Log.e(TAG, "无效的小时值: " + hourOfDay); return; } if (mState != MODE_INITIALIZING && hourOfDay == getCurrentHourOfDay()) { return; } mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); // 更新AM/PM状态 if (!mIs24HourView) { mIsAm = (hourOfDay < 12); updateAmPmControl(); } // 更新显示值 mHourSpinner.setValue(getCurrentDisplayHour()); onDateTimeChanged(); } /** * 获取当前分钟 */ public int getCurrentMinute() { return mDate.get(Calendar.MINUTE); } /** * 设置当前分钟 */ public void setCurrentMinute(int minute) { if (mState != MODE_INITIALIZING && minute == getCurrentMinute()) { return; } mMinuteSpinner.setValue(minute); mDate.set(Calendar.MINUTE, minute); onDateTimeChanged(); } /** * 是否使用24小时制 */ public boolean is24HourView () { return mIs24HourView; } /** * 设置24小时制模式 */ public void set24HourView(boolean is24HourView) { if (mIs24HourView == is24HourView) { return; } mIs24HourView = is24HourView; // 更新AM/PM显示 int amPmVisibility = is24HourView ? View.GONE : View.VISIBLE; mAmPmSpinner.setVisibility(amPmVisibility); // 更新小时控制 updateHourControl(); setCurrentHour(getCurrentHourOfDay()); } /** * 设置最小可选日期 */ public void setMinDate(long minDate) { mMinDate = minDate; validateCurrentDate(); } /** * 设置最大可选日期 */ public void setMaxDate(long maxDate) { mMaxDate = maxDate; validateCurrentDate(); } /** * 设置日期时间变更监听器 */ public void setOnDateTimeChangedListener(OnDateTimeChangedListener listener) { mOnDateTimeChangedListener = listener; } /*************************** 私有方法 ***************************/ /** * 计算新小时值 (处理滚动越界) */ private int computeNewHour(int oldVal, int newVal) { int currentHour = getCurrentHourOfDay(); if (mIs24HourView) { // 24小时制处理 if (oldVal == HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW && newVal == 0) { return 0; // 23 -> 0 } else if (oldVal == 0 && newVal == HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW) { return 23; // 0 -> 23 } } else { // 12小时制处理 if (oldVal == 12 && newVal == 11) { return mIsAm ? 11 : 23; // PM状态下12->11实际上是23点 } else if (oldVal == 11 && newVal == 12) { return mIsAm ? 12 : 0; // AM状态下11->12是中午12点 } } // 基本转换 int hour = newVal; if (!mIs24HourView && !mIsAm && hour != 12) { hour += 12; } // 特殊处理中午12点 if (hour == 24) hour = 0; return hour; } /** * 更新日期控件显示 */ private void updateDateControl() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(mDate.getTimeInMillis()); // 以当前日期为中心生成一周的日期 cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2); for (int i = 0; i < DAYS_IN_ALL_WEEK; i++) { // 生成日期显示字符串 (带星期) mDateDisplayValues[i] = formatDateWithWeekday(cal); cal.add(Calendar.DAY_OF_YEAR, 1); } // 设置日期选择器显示内容 mDateSpinner.setDisplayedValues(mDateDisplayValues); mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 当前日期在中间位置 } /** * 格式化日期 (带星期显示) */ private String formatDateWithWeekday(Calendar calendar) { java.text.DateFormat dateFormat = java.text.DateFormat.getDateInstance( java.text.DateFormat.MEDIUM, Locale.getDefault()); // 添加星期显示 String weekday = getWeekdayName(calendar.get(Calendar.DAY_OF_WEEK)); return String.format("%s (%s)", dateFormat.format(calendar.getTime()), weekday ); } /** * 获取本地化星期名称 */ private String getWeekdayName(int dayOfWeek) { String[] weekdays = new DateFormatSymbols().getWeekdays(); if (dayOfWeek >= Calendar.SUNDAY && dayOfWeek <= Calendar.SATURDAY) { return weekdays[dayOfWeek]; } return ""; } /** * 更新AM/PM控件 */ private void updateAmPmControl() { if (mIs24HourView) { return; } mAmPmSpinner.setValue(mIsAm ? 0 : 1); } /** * 更新小时控件范围 */ 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); } mHourSpinner.setValue(getCurrentDisplayHour()); } /** * 更新AM/PM符号 */ private void updateAmPmSymbols() { String[] ampmSymbols = new DateFormatSymbols().getAmPmStrings(); if (ampmSymbols.length < 2) { Log.e(TAG, "AM/PM符号获取失败"); ampmSymbols = new String[]{"AM", "PM"}; // 默认值 } mAmPmSpinner.setDisplayedValues(ampmSymbols); } /** * 应用深色模式适配 */ private void applyDarkMode() { // 在Android 10+系统上添加深色模式支持 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { setForceDarkAllowed(true); } } /** * 验证当前日期是否在允许范围内 */ private void validateCurrentDate() { long currentTime = getCurrentDateInTimeMillis(); if (currentTime < mMinDate || currentTime > mMaxDate) { // 自动调整到有效范围 long newTime = Math.max(mMinDate, Math.min(currentTime, mMaxDate)); setCurrentDate(newTime); // 触发更新 updateDateControl(); updateHourControl(); updateAmPmControl(); } } /** * 检查当前日期是否在允许范围内 */ private boolean isDateInRange() { long currentTime = mDate.getTimeInMillis(); return currentTime >= mMinDate && currentTime <= mMaxDate; } /** * 回滚日期变化 */ private void revertDateChange() { // 回滚到上一次有效日期 if (mDateSpinner.getValue() > DAYS_IN_ALL_WEEK / 2) { mDateSpinner.setValue(mDateSpinner.getValue() - 1); } else if (mDateSpinner.getValue() < DAYS_IN_ALL_WEEK / 2) { mDateSpinner.setValue(mDateSpinner.getValue() + 1); } } /** * 显示日期范围警告 */ private void showDateRangeWarning() { Context context = getContext(); Calendar minCal = Calendar.getInstance(); minCal.setTimeInMillis(mMinDate); Calendar maxCal = Calendar.getInstance(); maxCal.setTimeInMillis(mMaxDate); java.text.DateFormat df = java.text.DateFormat.getDateInstance(); String msg = String.format( context.getString(R.string.date_range_warning), df.format(minCal.getTime()), df.format(maxCal.getTime()) ); Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } /** * 触发日期时间变更事件 */ private void onDateTimeChanged() { if (mOnDateTimeChangedListener != null) { mOnDateTimeChangedListener.onDateTimeChanged( this, getCurrentYear(), getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute() ); } } }