背景
前段時間公司測試給我提了一個bug:在日期選擇框彈出來的時候,顯示出了未來1個月的日期,如下所示:

需求是說用戶無法選擇今天以后的日期,所以要將未來的日期給隱藏掉。
探索
所以,我立刻去查看了下自己的代碼:
long nowTime = System.currentTimeMillis();
mBinding.dataPicker.setMinDate(DateTimeUtils.formatDateString("2000-01-01"));
mBinding.dataPicker.setMaxDate(nowTime);
獲取當(dāng)前的時間,然后將當(dāng)前的時間設(shè)置為最大日期??戳艘槐樗坪鯖]有多大問題,那么為什么會多了一個月的日期顯示呢。
怎么辦呢?那就查看源碼吧。
既然日期選擇框顯示了未來一個月的日期,那么先去查看下這個DataPicker是怎么繪制出來的吧
switch (mMode) {
case MODE_CALENDAR:
mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes);
break;
case MODE_SPINNER:
default:
mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes);
break;
}
在DataPicker的構(gòu)造函數(shù)里面初始化了Delegate,我們沒有設(shè)置屬性,那就是默認(rèn)的DatePickerSpinnerDelegate實現(xiàn)類。既然是代理,那么后續(xù)的操作應(yīng)該都是在delegate實現(xiàn)類里面做了,那么進(jìn)入DatePickerSpinnerDelegate一探究竟。
DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
...代碼省略...
// day
mDaySpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.day);
mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
mDaySpinner.setOnLongPressUpdateInterval(100);
mDaySpinner.setOnValueChangedListener(onChangeListener);
mDaySpinnerInput = (EditText) mDaySpinner.findViewById(com.android.internal.R.id.numberpicker_input);
// month
mMonthSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.month);
mMonthSpinner.setMinValue(0);
mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
mMonthSpinner.setDisplayedValues(mShortMonths);
mMonthSpinner.setOnLongPressUpdateInterval(200);
mMonthSpinner.setOnValueChangedListener(onChangeListener);
mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(com.android.internal.R.id.numberpicker_input);
// year
mYearSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.year);
mYearSpinner.setOnLongPressUpdateInterval(100);
mYearSpinner.setOnValueChangedListener(onChangeListener);
...代碼省略...
}
一眼掃過來,發(fā)現(xiàn)了這三個spinner,看名字應(yīng)該就是彈窗上面顯示的年月日的控件。這里顯示了未來一個月的日期,那我們就只關(guān)心mMonthSpinner是怎么繪制出來的吧,去查看下這個對象里面的onDraw方法
protected void onDraw(Canvas canvas) {
...代碼省略...
// draw the selector wheel
int[] selectorIndices = mSelectorIndices;
for (int i = 0; i < selectorIndices.length; i++) {
int selectorIndex = selectorIndices[i];
String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
// Do not draw the middle item if input is visible since the input
// is shown only if the wheel is static and it covers the middle
// item. Otherwise, if the user starts editing the text via the
// IME he may see a dimmed version of the old value intermixed
// with the new one.
if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) ||
(i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) {
canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
}
y += mSelectorElementHeight;
}
...代碼省略...
}
同樣我們也只找重點(找drawText即可),發(fā)現(xiàn)是mSelectorIndices[]這個數(shù)組決定的要繪制的月份。那整個類里面搜索一下這個數(shù)組什么時候被賦值的
/**
* Resets the selector indices and clear the cached string representation of
* these indices.
*/
private void initializeSelectorWheelIndices() {
mSelectorIndexToStringCache.clear();
int[] selectorIndices = mSelectorIndices;
int current = getValue();
for (int i = 0; i < mSelectorIndices.length; i++) {
int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
if (mWrapSelectorWheel) {
selectorIndex = getWrappedSelectorIndex(selectorIndex);
}
selectorIndices[i] = selectorIndex;
ensureCachedScrollSelectorValue(selectorIndices[i]);
}
}
搜了一圈發(fā)現(xiàn)只在這個方法里面被賦值過,然后在查看下這個方法的調(diào)用地方

找到了setMaxValue方法,看來似乎離真相越來越近了,那么這個setMaxValue到底做了什么呢?接著往下看
public void setMaxValue(int maxValue) {
if (mMaxValue == maxValue) {
return;
}
if (maxValue < 0) {
throw new IllegalArgumentException("maxValue must be >= 0");
}
mMaxValue = maxValue;
if (mMaxValue < mValue) {
mValue = mMaxValue;
}
updateWrapSelectorWheel();
initializeSelectorWheelIndices();
updateInputTextView();
tryComputeMaxWidth();
invalidate();
}
這個setMaxValue只是把maxValue值設(shè)置了進(jìn)來,然后在initializeSelectorWheelIndices對數(shù)組進(jìn)行了賦值,看來還是得往更上層找,這個maxValue值到底怎么傳的。

因為我們現(xiàn)在設(shè)置的是日期,那么必然就看DatePickerSpinnerDelegatede#updateSpinner就好了。
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);
}
...代碼省略...
}
看這段代碼,當(dāng)mCurrentDate等于mMaxDate的時候,就將當(dāng)前日期的月份設(shè)置到mMonthSpinner的maxValue里面去,看上去也沒啥問題???難道m(xù)CurrentDate和mMaxDate不相等?在去找下mCurrentDate是怎么初始化的
// initialize to current date
mCurrentDate.setTimeInMillis(System.currentTimeMillis());
在DatePickerSpinnerDelegatede的構(gòu)造方法里面設(shè)置了當(dāng)前時間,mMaxDate初始化的時候賦的值也是System.currentTimeMillis。一般來說這兩個時間的年月日應(yīng)該是相等的,莫非.equal方法判斷的時候算上了時分秒?再去找一下.equal方法的邏輯。
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
try {
Calendar that = (Calendar)obj;
return compareTo(getMillisOf(that)) == 0 &&
lenient == that.lenient &&
firstDayOfWeek == that.firstDayOfWeek &&
minimalDaysInFirstWeek == that.minimalDaysInFirstWeek &&
zone.equals(that.zone);
} catch (Exception e) {
// Note: GregorianCalendar.computeTime throws
// IllegalArgumentException if the ERA value is invalid
// even it's in lenient mode.
}
return false;
}
private int compareTo(long t) {
long thisTime = getMillisOf(this);
return (thisTime > t) ? 1 : (thisTime == t) ? 0 : -1;
}
看這段代碼發(fā)現(xiàn),果然將兩個時間戳做了比較。這兩個時間戳調(diào)用System.currentTimeMillis的時機都不一樣,那肯定是不可能相等的。那么也就是說你在外部獲取的時間肯定不可能跟mCurrentDate一致,所以設(shè)置最大日期的時候,一定會出問題。
解決方案
既然外面設(shè)置的mMaxDate無法跟里面的mCurrentDate保持一致,那我直接反射修改里面的mCurrentDate不就可以了?說干就干于是就開始寫了反射的代碼:
try {
Field dataPickerSpinnerDelegateField = mBinding.dataPicker.getClass().getDeclaredField("mDelegate");
dataPickerSpinnerDelegateField.setAccessible(true);
Object dataPickerSpinnerDelegate = dataPickerSpinnerDelegateField.get(mBinding.dataPicker);
Field currentDateField = dataPickerSpinnerDelegate.getClass().getDeclaredField("mCurrentDate");
currentDateField.setAccessible(true);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
Calendar currentDate = (Calendar) currentDateField.get(dataPickerSpinnerDelegate);
currentDate.setTimeInMillis(nowTime);
} else {
java.util.Calendar currentDate = (java.util.Calendar) currentDateField.get(dataPickerSpinnerDelegate);
currentDate.setTimeInMillis(nowTime);
}
} catch (Exception e) {
e.printStackTrace();
}
因為這個mDelegate的實現(xiàn)類被隱藏了,所以我們在反射獲取這個類的時候直接用Object就可以了。寫完這段代碼想想應(yīng)該能成功吧?

生活總是不會跟你想象的一樣,mCurrentDate無法通過反射獲取(后來試了其他的板子是可以獲取到的,發(fā)現(xiàn)似乎是Pixel的獲取不到)。這下就悲催了,反射獲取不到。那么我們查看下setMaxDate,看看有什么蛛絲馬跡可以找到
@Override
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)) {
// Same day, no-op.
return;
}
mMaxDate.setTimeInMillis(maxDate);
mCalendarView.setMaxDate(maxDate);
if (mCurrentDate.after(mMaxDate)) {
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
updateCalendarView();
}
updateSpinners();
}
mCurrentDate設(shè)置的日期大于mMaxDate的時候mCurrentDate就會設(shè)置mMaxDate的時間,這樣不就好了?而且DatePicker也提供了獲取年月日的方法,想到這里就去試試
final int currYear = mBinding.dataPicker.getYear();
final int currMonth = mBinding.dataPicker.getMonth();
final int currDay = mBinding.dataPicker.getDayOfMonth();
Calendar calendar = Calendar.getInstance();
calendar.set(currYear,currMonth,currDay,0,0,0);
mBinding.dataPicker.setMinDate(DateTimeUtils.formatDateString("2000-01-01"));
mBinding.dataPicker.setMaxDate(calendar.getTimeInMillis());
通過DataPicker獲取mCurrentDate的年月日,然后初始化一個Calendar,給他設(shè)置時間為0點0分0秒,在將這個Calendar作為MaxDate傳進(jìn)去,這樣就能保證傳入的MaxDate一定小于mCurrentDate。然后run一把

完美!
總結(jié)
總的來說,不是什么大問題,主要還是感覺這個DataPicker設(shè)置最大值的邏輯還是有點奇怪(不知道是不是谷歌開發(fā)者故意這么設(shè)計的)。