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