You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

684 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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()
);
}
}
}