日期选择器timeInMillis must be between the values of getMinDate() and getMaxDate()

1,697 阅读4分钟

一:bug描述

DatePickerDialog dialog = new DatePickerDialog(getContext(), new OnDateSetListener() {...}, year, month, day);
dialog.show();

当这个弹窗出现后,其时间范围是1900/1/1 ~ 2100/12/31。当滚动到1900/1/9时后,发现月份可以继续滑动,将月份继续滑动至12月时,会出现崩溃.

崩溃图片

崩溃日志如下:

java.lang.IllegalArgumentException: timeInMillis must be between the values of getMinDate() and getMaxDate()
	at android.widget.CalendarViewLegacyDelegate.goTo(CalendarViewLegacyDelegate.java:798)
	at android.widget.CalendarViewLegacyDelegate.setDate(CalendarViewLegacyDelegate.java:591)
	at android.widget.CalendarView.setDate(CalendarView.java:547)
	at android.widget.DatePickerSpinnerDelegate.updateCalendarView(DatePickerSpinnerDelegate.java:568)
	at android.widget.DatePickerSpinnerDelegate.-wrap2(Unknown Source:0)
	at android.widget.DatePickerSpinnerDelegate$1.onValueChange(DatePickerSpinnerDelegate.java:152)
	at android.widget.NumberPicker.notifyChange(NumberPicker.java:1981)
	at android.widget.NumberPicker.setValueInternal(NumberPicker.java:1752)
	at android.widget.NumberPicker.scrollBy(NumberPicker.java:1143)
	at android.widget.NumberPicker.onTouchEvent(NumberPicker.java:932)
	at android.view.View.dispatchTouchEvent(View.java:11823)

此bug并非必现,目前线上发现崩溃手机如下,貌似只有android8.1.0系统才会出现,而且看堆栈日志也是系统代码日志,因此认为是系统代码问题:

二:问题分析 看堆栈日志可以发现,出问题代码在android.widget.CalendarViewLegacyDelegate#goTo中:

会比较date(当前日期选择器即将选中的值)和mMinDate(日期选择器最小时间)、mMaxDate(日期选择器最大时间)的大小,通过查看before和after的源码可以发现,Calendar的这两个方法比较的是android.icu.util.Calendar内部的一个时间毫秒值。

public boolean before(Object when) {
    return compare(when) < 0;
}

public boolean after(Object when) {
    return compare(when) > 0;
}

public int compareTo(Calendar that) {
    long v = getTimeInMillis() - that.getTimeInMillis();
    return v < 0 ? -1 : (v > 0 ? 1 : 0);
}

找了2部手机,崩溃手机A和不崩溃手机B,观察在崩溃时这三者的值:

  • mMinDate.getTimeInMills():A、B均为(-2209017600000),转化成时间:1900/1/1 0:5:43
  • mMaxDate.getTimeInMills():A、B均为(4133865600000),转化成时间:2100/12/31 0:0:0
  • date.getTimeInMills():A为(-2209017943000),转化成时间为:1900/1/1 0:0:0, B为(-2209017600000),转化成时间:1900/1/1 0:5:43

从上述结果中可以看出,主要原因是A手机在继续滑动时,传入的时间小于mMinDate,所以导致崩溃。

解决: 由于date参数值是系统内部代码传入,我们无法更改,但可以设置DatePickDialog的最小/大时间。show u my code:

DatePickerDialog dialog = new DatePickerDialog(getContext(), new OnDateSetListener() {...}, year, month, day);
Calendar calendar = Calendar.getInstance();
//设置日期最小值,今日 - 120年,时间为当日0:0:0:0
calendar.add(Calendar.YEAR, -120);
calendar.set(Calendar.MONTH, calendar.getMinimum(Calendar.MONTH));
calendar.set(Calendar.DAY_OF_MONTH, calendar.getMinimum(Calendar.DAY_OF_MONTH));
calendar.set(Calendar.HOUR_OF_DAY, calendar.getMinimum(Calendar.HOUR_OF_DAY));
calendar.set(Calendar.MINUTE, calendar.getMinimum(Calendar.MINUTE));
calendar.set(Calendar.SECOND, calendar.getMinimum(Calendar.SECOND));
calendar.set(Calendar.MILLISECOND, calendar.getMinimum(Calendar.MILLISECOND));
dialog.getDatePicker().setMinDate(calendar.getTimeInMillis());
dialog.show();

运行一下,发现依旧崩溃。。。why?断点调试后,在上述代码倒数第二行,获取calendar.getTimeInMills发现值依旧是-2209017600000(1900/1/1 0:5:43)??我明明代码设置的是1900/1/1 0:00:00。

这就要引出另一个问题了,测试代码如下:

Calendar calendar = Calendar.getInstance();

calendar.add(Calendar.YEAR, -2);
System.out.println(calendar.getTimeInMillis()); //1528526099728:2018/6/9 14:34:59

calendar.add(Calendar.YEAR, -50);
System.out.println(calendar.getTimeInMillis()); //-49310700272:1968/6/9 14:34:59

calendar.add(Calendar.YEAR, -50);
System.out.println(calendar.getTimeInMillis()); //-1627233900272:1918/6/9 14:34:59

calendar.add(Calendar.YEAR, -50);
System.out.println(calendar.getTimeInMillis()); //-3204984300272:1868/6/9 14:40:42

从上述代码可以发现,当calendar时间往前太久,getTimeInMills会出现错误,在当前年份-152年后,获取时间发现日期是对对,可是时间不对。因此使用calendar减时间时,不要减太久了。。。即使向上面代码手动强制设置时分秒,获取到的timeInMills也是不对的,这个问题无关手机系统,试了两部手机都是这样。于是我将上面代码中,-120年改成了-100年就不会再崩溃。

再回想一下,为什么刚刚两部手机,它们的mMinDate都是1900/1/1 0:5:43,而不是1900/1/1 0:00:00,我想也是因为calendar时间往前太久的缘故。但是虽然两部手机的mMinDate都是一样的时间,但是不同的手机,可能对参数date也做了容错处理,A手机中date传入的就是选中日期的0:00:00的毫秒值,而B手机则会容错,保证date是大于等于最小值的。