DatePicker最大日期顯示問題

背景

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


Screenshot_20200717-161109.png

需求是說用戶無法選擇今天以后的日期,所以要將未來的日期給隱藏掉。


探索

所以,我立刻去查看了下自己的代碼:

        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)用地方


image.png

找到了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值到底怎么傳的。


image.png

因為我們現(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)該能成功吧?


image.png

生活總是不會跟你想象的一樣,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一把


image.png

完美!


總結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容