DatePicker.java revision c39d9c75590eca86a5e7e32a8824ba04a0d42e9b
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package android.support.v17.leanback.widget.picker;
16
17import android.content.Context;
18import android.content.res.TypedArray;
19import android.support.annotation.RestrictTo;
20import android.support.v17.leanback.R;
21import android.text.TextUtils;
22import android.util.AttributeSet;
23import android.util.Log;
24
25import java.text.DateFormat;
26import java.text.ParseException;
27import java.text.SimpleDateFormat;
28import java.util.ArrayList;
29import java.util.Calendar;
30import java.util.Locale;
31import java.util.TimeZone;
32
33import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
34
35/**
36 * {@link DatePicker} is a directly subclass of {@link Picker}.
37 * This class is a widget for selecting a date. The date can be selected by a
38 * year, month, and day Columns. The "minDate" and "maxDate" from which dates to be selected
39 * can be customized.  The columns can be customized by attribute "datePickerFormat" or
40 * {@link #setDatePickerFormat(String)}.
41 *
42 * @attr ref R.styleable#lbDatePicker_android_maxDate
43 * @attr ref R.styleable#lbDatePicker_android_minDate
44 * @attr ref R.styleable#lbDatePicker_datePickerFormat
45 * @hide
46 */
47@RestrictTo(GROUP_ID)
48public class DatePicker extends Picker {
49
50    static final String LOG_TAG = "DatePicker";
51
52    private String mDatePickerFormat;
53    PickerColumn mMonthColumn;
54    PickerColumn mDayColumn;
55    PickerColumn mYearColumn;
56    int mColMonthIndex;
57    int mColDayIndex;
58    int mColYearIndex;
59
60    final static String DATE_FORMAT = "MM/dd/yyyy";
61    final DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
62    PickerConstant mConstant;
63
64    Calendar mMinDate;
65    Calendar mMaxDate;
66    Calendar mCurrentDate;
67    Calendar mTempDate;
68
69    public DatePicker(Context context, AttributeSet attrs) {
70        this(context, attrs, 0);
71    }
72
73    public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) {
74        super(context, attrs, defStyleAttr);
75
76        updateCurrentLocale();
77        setSeparator(mConstant.dateSeparator);
78
79        final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
80                R.styleable.lbDatePicker);
81        String minDate = attributesArray.getString(R.styleable.lbDatePicker_android_minDate);
82        String maxDate = attributesArray.getString(R.styleable.lbDatePicker_android_maxDate);
83        mTempDate.clear();
84        if (!TextUtils.isEmpty(minDate)) {
85            if (!parseDate(minDate, mTempDate)) {
86                mTempDate.set(1900, 0, 1);
87            }
88        } else {
89            mTempDate.set(1900, 0, 1);
90        }
91        mMinDate.setTimeInMillis(mTempDate.getTimeInMillis());
92
93        mTempDate.clear();
94        if (!TextUtils.isEmpty(maxDate)) {
95            if (!parseDate(maxDate, mTempDate)) {
96                mTempDate.set(2100, 0, 1);
97            }
98        } else {
99            mTempDate.set(2100, 0, 1);
100        }
101        mMaxDate.setTimeInMillis(mTempDate.getTimeInMillis());
102
103        String datePickerFormat = attributesArray
104                .getString(R.styleable.lbDatePicker_datePickerFormat);
105        if (TextUtils.isEmpty(datePickerFormat)) {
106            datePickerFormat = new String(
107                    android.text.format.DateFormat.getDateFormatOrder(context));
108        }
109        setDatePickerFormat(datePickerFormat);
110    }
111
112    private boolean parseDate(String date, Calendar outDate) {
113        try {
114            outDate.setTime(mDateFormat.parse(date));
115            return true;
116        } catch (ParseException e) {
117            Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
118            return false;
119        }
120    }
121
122    /**
123     * Changes format of showing dates.  For example "YMD".
124     * @param datePickerFormat Format of showing dates.
125     */
126    public void setDatePickerFormat(String datePickerFormat) {
127        if (TextUtils.isEmpty(datePickerFormat)) {
128            datePickerFormat = new String(
129                    android.text.format.DateFormat.getDateFormatOrder(getContext()));
130        }
131        datePickerFormat = datePickerFormat.toUpperCase();
132        if (TextUtils.equals(mDatePickerFormat, datePickerFormat)) {
133            return;
134        }
135        mDatePickerFormat = datePickerFormat;
136        mYearColumn = mMonthColumn = mDayColumn = null;
137        mColYearIndex = mColDayIndex = mColMonthIndex = -1;
138        ArrayList<PickerColumn> columns = new ArrayList<PickerColumn>(3);
139        for (int i = 0; i < datePickerFormat.length(); i++) {
140            switch (datePickerFormat.charAt(i)) {
141            case 'Y':
142                if (mYearColumn != null) {
143                    throw new IllegalArgumentException("datePicker format error");
144                }
145                columns.add(mYearColumn = new PickerColumn());
146                mColYearIndex = i;
147                mYearColumn.setLabelFormat("%d");
148                break;
149            case 'M':
150                if (mMonthColumn != null) {
151                    throw new IllegalArgumentException("datePicker format error");
152                }
153                columns.add(mMonthColumn = new PickerColumn());
154                mMonthColumn.setStaticLabels(mConstant.months);
155                mColMonthIndex = i;
156                break;
157            case 'D':
158                if (mDayColumn != null) {
159                    throw new IllegalArgumentException("datePicker format error");
160                }
161                columns.add(mDayColumn = new PickerColumn());
162                mDayColumn.setLabelFormat("%02d");
163                mColDayIndex = i;
164                break;
165            default:
166                throw new IllegalArgumentException("datePicker format error");
167            }
168        }
169        setColumns(columns);
170        updateSpinners(false);
171    }
172
173    /**
174     * Get format of showing dates.  For example "YMD".  Default value is from
175     * {@link android.text.format.DateFormat#getDateFormatOrder(Context)}.
176     * @return Format of showing dates.
177     */
178    public String getDatePickerFormat() {
179        return mDatePickerFormat;
180    }
181
182    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
183        if (oldCalendar == null) {
184            return Calendar.getInstance(locale);
185        } else {
186            final long currentTimeMillis = oldCalendar.getTimeInMillis();
187            Calendar newCalendar = Calendar.getInstance(locale);
188            newCalendar.setTimeInMillis(currentTimeMillis);
189            return newCalendar;
190        }
191    }
192
193    private void updateCurrentLocale() {
194        mConstant = new PickerConstant(Locale.getDefault(), getContext().getResources());
195        mTempDate = getCalendarForLocale(mTempDate, mConstant.locale);
196        mMinDate = getCalendarForLocale(mMinDate, mConstant.locale);
197        mMaxDate = getCalendarForLocale(mMaxDate, mConstant.locale);
198        mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale);
199
200        if (mMonthColumn != null) {
201            mMonthColumn.setStaticLabels(mConstant.months);
202            setColumnAt(mColMonthIndex, mMonthColumn);
203        }
204    }
205
206    @Override
207    public final void onColumnValueChanged(int column, int newVal) {
208        mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
209        // take care of wrapping of days and months to update greater fields
210        int oldVal = getColumnAt(column).getCurrentValue();
211        if (column == mColDayIndex) {
212            mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
213        } else if (column == mColMonthIndex) {
214            mTempDate.add(Calendar.MONTH, newVal - oldVal);
215        } else if (column == mColYearIndex) {
216            mTempDate.add(Calendar.YEAR, newVal - oldVal);
217        } else {
218            throw new IllegalArgumentException();
219        }
220        setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
221                mTempDate.get(Calendar.DAY_OF_MONTH));
222        updateSpinners(false);
223    }
224
225
226    /**
227     * Sets the minimal date supported by this {@link DatePicker} in
228     * milliseconds since January 1, 1970 00:00:00 in
229     * {@link TimeZone#getDefault()} time zone.
230     *
231     * @param minDate The minimal supported date.
232     */
233    public void setMinDate(long minDate) {
234        mTempDate.setTimeInMillis(minDate);
235        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
236                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
237            return;
238        }
239        mMinDate.setTimeInMillis(minDate);
240        if (mCurrentDate.before(mMinDate)) {
241            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
242        }
243        updateSpinners(false);
244    }
245
246
247    /**
248     * Gets the minimal date supported by this {@link DatePicker} in
249     * milliseconds since January 1, 1970 00:00:00 in
250     * {@link TimeZone#getDefault()} time zone.
251     * <p>
252     * Note: The default minimal date is 01/01/1900.
253     * <p>
254     *
255     * @return The minimal supported date.
256     */
257    public long getMinDate() {
258        return mMinDate.getTimeInMillis();
259    }
260
261    /**
262     * Sets the maximal date supported by this {@link DatePicker} in
263     * milliseconds since January 1, 1970 00:00:00 in
264     * {@link TimeZone#getDefault()} time zone.
265     *
266     * @param maxDate The maximal supported date.
267     */
268    public void setMaxDate(long maxDate) {
269        mTempDate.setTimeInMillis(maxDate);
270        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
271                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
272            return;
273        }
274        mMaxDate.setTimeInMillis(maxDate);
275        if (mCurrentDate.after(mMaxDate)) {
276            mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
277        }
278        updateSpinners(false);
279    }
280
281    /**
282     * Gets the maximal date supported by this {@link DatePicker} in
283     * milliseconds since January 1, 1970 00:00:00 in
284     * {@link TimeZone#getDefault()} time zone.
285     * <p>
286     * Note: The default maximal date is 12/31/2100.
287     * <p>
288     *
289     * @return The maximal supported date.
290     */
291    public long getMaxDate() {
292        return mMaxDate.getTimeInMillis();
293    }
294
295    /**
296     * Gets current date value in milliseconds since January 1, 1970 00:00:00 in
297     * {@link TimeZone#getDefault()} time zone.
298     *
299     * @return Current date values.
300     */
301    public long getDate() {
302        return mCurrentDate.getTimeInMillis();
303    }
304
305    private void setDate(int year, int month, int dayOfMonth) {
306        mCurrentDate.set(year, month, dayOfMonth);
307        if (mCurrentDate.before(mMinDate)) {
308            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
309        } else if (mCurrentDate.after(mMaxDate)) {
310            mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
311        }
312    }
313
314    /**
315     * Update the current date.
316     *
317     * @param year The year.
318     * @param month The month which is <strong>starting from zero</strong>.
319     * @param dayOfMonth The day of the month.
320     * @param animation True to run animation to scroll the column.
321     */
322    public void updateDate(int year, int month, int dayOfMonth, boolean animation) {
323        if (!isNewDate(year, month, dayOfMonth)) {
324            return;
325        }
326        setDate(year, month, dayOfMonth);
327        updateSpinners(animation);
328    }
329
330    private boolean isNewDate(int year, int month, int dayOfMonth) {
331        return (mCurrentDate.get(Calendar.YEAR) != year
332                || mCurrentDate.get(Calendar.MONTH) != dayOfMonth
333                || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
334    }
335
336    private static boolean updateMin(PickerColumn column, int value) {
337        if (value != column.getMinValue()) {
338            column.setMinValue(value);
339            return true;
340        }
341        return false;
342    }
343
344    private static boolean updateMax(PickerColumn column, int value) {
345        if (value != column.getMaxValue()) {
346            column.setMaxValue(value);
347            return true;
348        }
349        return false;
350    }
351
352    private static int[] DATE_FIELDS = {Calendar.DAY_OF_MONTH, Calendar.MONTH, Calendar.YEAR};
353
354    // Following implementation always keeps up-to-date date ranges (min & max values) no matter
355    // what the currently selected date is. This prevents the constant updating of date values while
356    // scrolling vertically and thus fixes the animation jumps that used to happen when we reached
357    // the endpoint date field values since the adapter values do not change while scrolling up
358    // & down across a single field.
359    void updateSpinnersImpl(boolean animation) {
360        // set the spinner ranges respecting the min and max dates
361        int dateFieldIndices[] = {mColDayIndex, mColMonthIndex, mColYearIndex};
362
363        boolean allLargerDateFieldsHaveBeenEqualToMinDate = true;
364        boolean allLargerDateFieldsHaveBeenEqualToMaxDate = true;
365        for(int i = DATE_FIELDS.length - 1; i >= 0; i--) {
366            boolean dateFieldChanged = false;
367            if (dateFieldIndices[i] < 0)
368                continue;
369
370            int currField = DATE_FIELDS[i];
371            PickerColumn currPickerColumn = getColumnAt(dateFieldIndices[i]);
372
373            if (allLargerDateFieldsHaveBeenEqualToMinDate) {
374                dateFieldChanged |= updateMin(currPickerColumn,
375                        mMinDate.get(currField));
376            } else {
377                dateFieldChanged |= updateMin(currPickerColumn,
378                        mCurrentDate.getActualMinimum(currField));
379            }
380
381            if (allLargerDateFieldsHaveBeenEqualToMaxDate) {
382                dateFieldChanged |= updateMax(currPickerColumn,
383                        mMaxDate.get(currField));
384            } else {
385                dateFieldChanged |= updateMax(currPickerColumn,
386                        mCurrentDate.getActualMaximum(currField));
387            }
388
389            allLargerDateFieldsHaveBeenEqualToMinDate &=
390                    (mCurrentDate.get(currField) == mMinDate.get(currField));
391            allLargerDateFieldsHaveBeenEqualToMaxDate &=
392                    (mCurrentDate.get(currField) == mMaxDate.get(currField));
393
394            if (dateFieldChanged) {
395                setColumnAt(dateFieldIndices[i], currPickerColumn);
396            }
397            setColumnValue(dateFieldIndices[i], mCurrentDate.get(currField), animation);
398        }
399    }
400
401    private void updateSpinners(final boolean animation) {
402        // update range in a post call.  The reason is that RV does not allow notifyDataSetChange()
403        // in scroll pass.  UpdateSpinner can be called in a scroll pass, UpdateSpinner() may
404        // notifyDataSetChange to update the range.
405        post(new Runnable() {
406            @Override
407            public void run() {
408                updateSpinnersImpl(animation);
409            }
410        });
411    }
412}