项目中需要使用一个日期时间选择弹窗。因为系统的日期选择和时间选择是分开的,而且显示效果与要求不一致,因此自定义时间日期弹窗。
思路:这种时间日期选择控件,与系统的有些差别,因此,需要重新设计。不论是年月日,还是时分秒
的单个选择的控件,都是一个数字滚轮的模式。因此基于系统的NumberPicker组件的基础上进行开发。
1、NumberPicker。系统的数字选择器没有开放设置分割线颜色,选择文字的颜色,文字大小等属性的
定制,因此采用一个继承类开放这些属性设置。
public JDNumberPicker(Context context) {
super(context, null);
}
public JDNumberPicker(Context context, AttributeSet attrs) {
super(context, attrs);
//视图初始化
initView(context, attrs);
}
public JDNumberPicker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//视图初始化
initView(context, attrs);
}
/**
* 视图初始化
*/
private void initView(Context context, AttributeSet attrs) {
if(null != attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.JDNumberPicker, 0, 0);
try{
if (a.hasValue(R.styleable.JDNumberPicker_jdSelectionDividerHeight)) {
mSelectionDividerHeight = a.getDimensionPixelSize(R.styleable.JDNumberPicker_jdSelectionDividerHeight, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT);
setDividerHeight(mSelectionDividerHeight);
}
if (a.hasValue(R.styleable.JDNumberPicker_jdSelectionDividersDistance)) {
mSelectionDividersDistance = a.getDimensionPixelSize(R.styleable.JDNumberPicker_jdSelectionDividersDistance, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE);
setDividersDistance(mSelectionDividersDistance);
}
if (a.hasValue(R.styleable.JDNumberPicker_jdSelectDividersColor)) {
mSelectionDividersColor = a.getColor(R.styleable.JDNumberPicker_jdSelectDividersColor, Color.GRAY);
setDividerColor(mSelectionDividersColor);
}
if (a.hasValue(R.styleable.JDNumberPicker_jdSelectTextColor)) {
mSelectionTextColor = a.getColor(R.styleable.JDNumberPicker_jdSelectTextColor, Color.WHITE);
setSelectionTextColor(mSelectionTextColor);
}
if (a.hasValue(R.styleable.JDNumberPicker_jdSelectTextSize)) {
mSelectionTextSize = a.getDimensionPixelSize(R.styleable.JDNumberPicker_jdSelectTextSize, UNSCALED_DEFAULT_SELECTION_TEXTSIZE);
setSelectionTextSize(mSelectionTextSize);
}
} finally {
a.recycle();
}
}
}
属性的设置方法,通过反射来实现。
/**
* 设置分割线颜色
*
* @param color : 颜色值
*/
public void setDividerColor(int color) {
try {
Field pf = NumberPicker.class.getDeclaredField("mSelectionDivider");
if(null != pf) {
pf.setAccessible(true);
try {
//设置分割线的颜色值
pf.set(this, new ColorDrawable(color));
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
2)日期选择组件。
日期选择组件可以通过三个数字滚轮选择组件实现,即三个JDNumberPicker。
/**
* 日期选择器
*/
public class JDDatePicker extends LinearLayout {
private static final String LOG_TAG = JDDatePicker.class.getSimpleName();
/**
* The callback used to indicate the user changes\d the date.
*/
public interface OnDateChangedListener {
/**
* Called upon a date change.
*
* @param view The view associated with this listener.
* @param year The year that was set.
* @param monthOfYear The month that was set (0-11) for compatibility
* with {@link java.util.Calendar}.
* @param dayOfMonth The day of the month that was set.
*/
void onDateChanged(JDDatePicker view, int year, int monthOfYear, int dayOfMonth);
}
private static final int DEFAULT_START_YEAR = 1900;
private static final int DEFAULT_END_YEAR = 2100;
/**
* 日期样式
*/
private static final String DATE_FORMAT = "MM/dd/yyyy";
/**
* 上下文
*/
private Context mContext;
private View root;
private String[] mShortMonths;
private java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
private int mNumberOfMonths;
private Calendar mTempDate;
private Calendar mMinDate;
private Calendar mMaxDate;
private Calendar mCurrentDate;
/**
* 日期变化监听
*/
private OnDateChangedListener mOnDateChangedListener;
/**
* 年份选择器
*/
private JDNumberPicker mYearSpinner;
/**
* 月份选择器
*/
private JDNumberPicker mMonthSpinner;
/**
* 日选择器
*/
private JDNumberPicker mDaySpinner;
/**
* 本地时区
*/
private Locale mCurrentLocale;
public JDDatePicker(Context context) {
this(context, null);
}
public JDDatePicker(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
//视图初始化
initView(context, attrs);
}
/**
* 控件初始化
*
* @param context
* @param attrs
*/
private void initView(Context context, AttributeSet attrs) {
// initialization based on locale
setCurrentLocale(Locale.getDefault());
//读取属性
int startYear = DEFAULT_START_YEAR;
int endYear = DEFAULT_END_YEAR;
String minDate = "";
String maxDate = "";
if(null != attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.JDDatePicker, 0, 0);
try{
if(a.hasValue(R.styleable.JDDatePicker_jdStartYear)) {
startYear = a.getInt(R.styleable.JDDatePicker_jdStartYear,
DEFAULT_START_YEAR);
}
if(a.hasValue(R.styleable.JDDatePicker_jdEndYear)) {
endYear = a.getInt(R.styleable.JDDatePicker_jdEndYear,
DEFAULT_END_YEAR);
}
if(a.hasValue(R.styleable.JDDatePicker_jdMinDate)) {
minDate = a.getString(R.styleable.JDDatePicker_jdMinDate);
}
if(a.hasValue(R.styleable.JDDatePicker_jdMaxDate)) {
maxDate = a.getString(R.styleable.JDDatePicker_jdMaxDate);
}
} finally {
a.recycle();
}
}
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.layout_date_picker, this,true);
mYearSpinner = (JDNumberPicker) findViewById(R.id.yearPicker);
mMonthSpinner = (JDNumberPicker) findViewById(R.id.monthPicker);
mDaySpinner = (JDNumberPicker) findViewById(R.id.dayPicker);
mYearSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
mMonthSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
mDaySpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
JDNumberPicker.OnValueChangeListener onChangeListener = new NumberPicker.OnValueChangeListener() {
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
// take care of wrapping of days and months to update greater fields
if (picker == mDaySpinner) {
int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
if (oldVal == maxDayOfMonth && newVal == 1) {
mTempDate.add(Calendar.DAY_OF_MONTH, 1);
} else if (oldVal == 1 && newVal == maxDayOfMonth) {
mTempDate.add(Calendar.DAY_OF_MONTH, -1);
} else {
mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
}
} else if (picker == mMonthSpinner) {
if (oldVal == 11 && newVal == 0) {
mTempDate.add(Calendar.MONTH, 1);
} else if (oldVal == 0 && newVal == 11) {
mTempDate.add(Calendar.MONTH, -1);
} else {
mTempDate.add(Calendar.MONTH, newVal - oldVal);
}
} else if (picker == mYearSpinner) {
mTempDate.set(Calendar.YEAR, newVal);
} else {
throw new IllegalArgumentException();
}
// now set the date to the adjusted one
setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
mTempDate.get(Calendar.DAY_OF_MONTH));
updateSpinners();
notifyDateChanged();
}
};
mDaySpinner.setFormatter(JDNumberPicker.getTwoDigitFormatter(mDaySpinner));
mDaySpinner.setOnLongPressUpdateInterval(100);
mDaySpinner.setOnValueChangedListener(onChangeListener);
// month
mMonthSpinner.setMinValue(0);
mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
mMonthSpinner.setDisplayedValues(mShortMonths);
mMonthSpinner.setOnLongPressUpdateInterval(200);
mMonthSpinner.setOnValueChangedListener(onChangeListener);
// year
mYearSpinner.setMinValue(startYear);
mYearSpinner.setMaxValue(endYear);
mYearSpinner.setOnLongPressUpdateInterval(100);
mYearSpinner.setOnValueChangedListener(onChangeListener);
// set the min date giving priority of the minDate over startYear
mTempDate.clear();
if (!TextUtils.isEmpty(minDate)) {
if (!parseDate(minDate, mTempDate)) {
mTempDate.set(startYear, 0, 1);
}
} else {
mTempDate.set(startYear, 0, 1);
}
setMinDate(mTempDate.getTimeInMillis());
// set the max date giving priority of the maxDate over endYear
mTempDate.clear();
if (!TextUtils.isEmpty(maxDate)) {
if (!parseDate(maxDate, mTempDate)) {
mTempDate.set(endYear, 11, 31);
}
} else {
mTempDate.set(endYear, 11, 31);
}
setMaxDate(mTempDate.getTimeInMillis());
// initialize to current date
mCurrentDate.setTimeInMillis(System.currentTimeMillis());
init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
.get(Calendar.DAY_OF_MONTH), null);
// If not explicitly specified this view is important for accessibility.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
onPopulateAccessibilityEvent(event);
return true;
}
@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(event);
final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
String selectedDateUtterance = DateUtils.formatDateTime(mContext,
mCurrentDate.getTimeInMillis(), flags);
event.getText().add(selectedDateUtterance);
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setClassName(DatePicker.class.getName());
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
info.setClassName(JDDatePicker.class.getName());
}
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setCurrentLocale(newConfig.locale);
}
// Override so we are in complete control of save / restore for this widget.
@Override
protected void dispatchRestoreInstanceState(SparseArray container) {
dispatchThawSelfOnly(container);
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
return new SavedState(superState, getYear(), getMonth(), getDayOfMonth());
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setDate(ss.mYear, ss.mMonth, ss.mDay);
updateSpinners();
}
/**
* Class for managing state storing/restoring.
*/
private static class SavedState extends BaseSavedState {
private final int mYear;
private final int mMonth;
private final int mDay;
/**
* Constructor called from {@link DatePicker#onSaveInstanceState()}
*/
private SavedState(Parcelable superState, int year, int month, int day) {
super(superState);
mYear = year;
mMonth = month;
mDay = day;
}
/**
* Constructor called from {@link #CREATOR}
*/
private SavedState(Parcel in) {
super(in);
mYear = in.readInt();
mMonth = in.readInt();
mDay = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mYear);
dest.writeInt(mMonth);
dest.writeInt(mDay);
}
@SuppressWarnings("all")
// suppress unused and hiding
public static final Parcelable.Creator CREATOR = new Creator() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
/**
* 设置选中项Text颜色
*
* @param color : 颜色值
*/
public void setSelectionTextColor(int color) {
mYearSpinner.setSelectionTextColor(color);
mMonthSpinner.setSelectionTextColor(color);
mDaySpinner.setSelectionTextColor(color);
}
/**
* 设置选中项分割线高度
*
* @param height : 选中项分割线高度
*/
public void setDividerHeight(int height) {
mYearSpinner.setDividerHeight(height);
mMonthSpinner.setDividerHeight(height);
mDaySpinner.setDividerHeight(height);
}
/**
* 设置分割线颜色
*
* @param color : 颜色值
*/
public void setDividerColor(int color) {
mYearSpinner.setDividerColor(color);
mMonthSpinner.setDividerColor(color);
mDaySpinner.setDividerColor(color);
}
/**
* 设置选项文字大小
*
* @param textSize : 文字大小
*/
public void setSelectionTextSize(int textSize) {
mYearSpinner.setSelectionTextSize(textSize);
mMonthSpinner.setSelectionTextSize(textSize);
mDaySpinner.setSelectionTextSize(textSize);
}
/**
* 设置选中项分割线之间距离
*
* @param distance : 选中项分割线之间距离
*/
public void setDividersDistance(int distance) {
mYearSpinner.setDividersDistance(distance);
mMonthSpinner.setDividersDistance(distance);
mDaySpinner.setDividersDistance(distance);
}
/**
* Updates the current date.
*
* @param year The year.
* @param month The month which is starting from zero.
* @param dayOfMonth The day of the month.
*/
public void updateDate(int year, int month, int dayOfMonth) {
if (!isNewDate(year, month, dayOfMonth)) {
return;
}
setDate(year, month, dayOfMonth);
updateSpinners();
notifyDateChanged();
}
/**
* Initialize the state. If the provided values designate an inconsistent
* date the values are normalized before updating the spinners.
*
* @param year The initial year.
* @param monthOfYear The initial month starting from zero.
* @param dayOfMonth The initial day of the month.
* @param onDateChangedListener How user is notified date is changed by
* user, can be null.
*/
public void init(int year, int monthOfYear, int dayOfMonth,
OnDateChangedListener onDateChangedListener) {
setDate(year, monthOfYear, dayOfMonth);
updateSpinners();
mOnDateChangedListener = onDateChangedListener;
}
/**
* 获取当前的日期
*
* @return
*/
public Calendar getSelectionDate() {
return mCurrentDate;
}
/**
* 设置日期选择监听
*
* @param onDateChangedListener How user is notified date is changed by
* user, can be null.
*/
public void setOnDateChangedListener(OnDateChangedListener onDateChangedListener) {
this.mOnDateChangedListener = onDateChangedListener;
}
private void updateSpinners() {
// set the spinner ranges respecting the min and max dates
if (mCurrentDate.equals(mMinDate)) {
mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
mDaySpinner.setWrapSelectorWheel(false);
mMonthSpinner.setDisplayedValues(null);
mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
mMonthSpinner.setWrapSelectorWheel(false);
} else if (mCurrentDate.equals(mMaxDate)) {
mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
mDaySpinner.setWrapSelectorWheel(false);
mMonthSpinner.setDisplayedValues(null);
mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
mMonthSpinner.setWrapSelectorWheel(false);
} else {
mDaySpinner.setMinValue(1);
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
mDaySpinner.setWrapSelectorWheel(true);
mMonthSpinner.setDisplayedValues(null);
mMonthSpinner.setMinValue(0);
mMonthSpinner.setMaxValue(11);
mMonthSpinner.setWrapSelectorWheel(true);
}
// make sure the month names are a zero based array
// with the months in the month spinner
String[] displayedValues = Arrays.copyOfRange(mShortMonths,
mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
mMonthSpinner.setDisplayedValues(displayedValues);
// year spinner range does not change based on the current date
mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
mYearSpinner.setWrapSelectorWheel(false);
// set the spinner values
mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
}
/**
* @return The selected year.
*/
public int getYear() {
return mCurrentDate.get(Calendar.YEAR);
}
/**
* @return The selected month.
*/
public int getMonth() {
return mCurrentDate.get(Calendar.MONTH);
}
/**
* @return The selected day of month.
*/
public int getDayOfMonth() {
return mCurrentDate.get(Calendar.DAY_OF_MONTH);
}
/**
* Notifies the listener, if such, for a change in the selected date.
*/
private void notifyDateChanged() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
if (mOnDateChangedListener != null) {
mOnDateChangedListener.onDateChanged(this, getYear(), getMonth(), getDayOfMonth());
}
}
/**
* Sets the maximal date supported by this {@link DatePicker} in
* milliseconds since January 1, 1970 00:00:00 in
* {@link TimeZone#getDefault()} time zone.
*
* @param maxDate The maximal supported date.
*/
public void setMaxDate(long maxDate) {
mTempDate.setTimeInMillis(maxDate);
if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
&& mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
return;
}
mMaxDate.setTimeInMillis(maxDate);
if (mCurrentDate.after(mMaxDate)) {
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
}
updateSpinners();
}
/**
* Sets the minimal date supported by this {@link NumberPicker} in
* milliseconds since January 1, 1970 00:00:00 in
* {@link TimeZone#getDefault()} time zone.
*
* @param minDate The minimal supported date.
*/
public void setMinDate(long minDate) {
mTempDate.setTimeInMillis(minDate);
if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
&& mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
return;
}
mMinDate.setTimeInMillis(minDate);
if (mCurrentDate.before(mMinDate)) {
mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
}
updateSpinners();
}
/**
* 设置日期(年月日)
*
* @param year : 年
* @param month : 月
* @param dayOfMonth : 月天数
*/
private void setDate(int year, int month, int dayOfMonth) {
mCurrentDate.set(year, month, dayOfMonth);
if (mCurrentDate.before(mMinDate)) {
mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
} else if (mCurrentDate.after(mMaxDate)) {
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
}
}
/**
* Parses the given date
and in case of success sets the result
* to the outDate
.
*
* @return True if the date was parsed.
*/
private boolean parseDate(String date, Calendar outDate) {
try {
outDate.setTime(mDateFormat.parse(date));
return true;
} catch (ParseException e) {
Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
return false;
}
}
/**
* 是否是一个新的日期
*
* @param year : 年
* @param month : 月
* @param dayOfMonth : 月天数
* @return
*/
private boolean isNewDate(int year, int month, int dayOfMonth) {
return (mCurrentDate.get(Calendar.YEAR) != year
|| mCurrentDate.get(Calendar.MONTH) != dayOfMonth
|| mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
}
/**
* Sets the current locale.
*
* @param locale The current locale.
*/
private void setCurrentLocale(Locale locale) {
if (locale.equals(mCurrentLocale)) {
return;
}
mCurrentLocale = locale;
mTempDate = getCalendarForLocale(mTempDate, locale);
mMinDate = getCalendarForLocale(mMinDate, locale);
mMaxDate = getCalendarForLocale(mMaxDate, locale);
mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
mShortMonths = new DateFormatSymbols().getShortMonths();
if (usingNumericMonths()) {
// We're in a locale where a date should either be all-numeric, or all-text.
// All-text would require custom NumberPicker formatters for day and year.
mShortMonths = new String[mNumberOfMonths];
for (int i = 0; i < mNumberOfMonths; ++i) {
mShortMonths[i] = String.format("%d", i + 1);
}
}
}
/**
* Tests whether the current locale is one where there are no real month names,
* such as Chinese, Japanese, or Korean locales.
*/
private boolean usingNumericMonths() {
return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0));
}
/**
* Gets a calendar for locale bootstrapped with the value of a given calendar.
*
* @param oldCalendar The old calendar.
* @param locale The locale.
*/
private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
if (oldCalendar == null) {
return Calendar.getInstance(locale);
} else {
final long currentTimeMillis = oldCalendar.getTimeInMillis();
Calendar newCalendar = Calendar.getInstance(locale);
newCalendar.setTimeInMillis(currentTimeMillis);
return newCalendar;
}
}
}
init:初始化方法设置初始化的日期和选择监听。
updateDate: 更新日期
3)时间选择器,同样是三个JDNumberPicker组成。采用24小时的模式计时。
4)时间日期选择器。通过AlertDialog,显示时间日期选择。参考JDDateTimePickerDialogUtil。
原项目代码:
https://github.com/joedan0104/DateTimePicker