DatePicker.java revision b19799d69cbbe7f7ca104520e9b07312ab7539af
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 */
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, int defStyleAttr) {
66        super(context, attrs, defStyleAttr);
67
68        updateCurrentLocale();
69
70        final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
71                R.styleable.lbDatePicker);
72        String minDate = attributesArray.getString(R.styleable.lbDatePicker_android_minDate);
73        String maxDate = attributesArray.getString(R.styleable.lbDatePicker_android_maxDate);
74        String datePickerFormat = attributesArray
75                .getString(R.styleable.lbDatePicker_datePickerFormat);
76        if (TextUtils.isEmpty(datePickerFormat)) {
77            datePickerFormat = new String(
78                    android.text.format.DateFormat.getDateFormatOrder(context));
79        }
80        setDatePickerFormat(datePickerFormat);
81
82        mTempDate.clear();
83        if (!TextUtils.isEmpty(minDate)) {
84            if (!parseDate(minDate, mTempDate)) {
85                mTempDate.set(1900, 0, 1);
86            }
87        } else {
88            mTempDate.set(1900, 0, 1);
89        }
90        setMinDate(mTempDate.getTimeInMillis());
91
92        mTempDate.clear();
93        if (!TextUtils.isEmpty(maxDate)) {
94            if (!parseDate(maxDate, mTempDate)) {
95                mTempDate.set(2100, 0, 1);
96            }
97        } else {
98            mTempDate.set(2100, 0, 1);
99        }
100        setMaxDate(mTempDate.getTimeInMillis());
101
102    }
103
104    private boolean parseDate(String date, Calendar outDate) {
105        try {
106            outDate.setTime(mDateFormat.parse(date));
107            return true;
108        } catch (ParseException e) {
109            Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
110            return false;
111        }
112    }
113
114    /**
115     * Changes format of showing dates, e.g. 'YMD'.
116     * @param datePickerFormat Format of showing dates.
117     */
118    public void setDatePickerFormat(String datePickerFormat) {
119        datePickerFormat = datePickerFormat.toUpperCase();
120        if (TextUtils.equals(mDatePickerFormat, datePickerFormat)) {
121            return;
122        }
123        mDatePickerFormat = datePickerFormat;
124        mYearColumn = mMonthColumn = mDayColumn = null;
125        mColYearIndex = mColDayIndex = mColMonthIndex = -1;
126        ArrayList<PickerColumn> columns = new ArrayList<PickerColumn>(3);
127        for (int i = 0; i < datePickerFormat.length(); i++) {
128            switch (datePickerFormat.charAt(i)) {
129            case 'Y':
130                if (mYearColumn != null) {
131                    throw new IllegalArgumentException("datePicker format error");
132                }
133                columns.add(mYearColumn = new PickerColumn());
134                mColYearIndex = i;
135                mYearColumn.setValueLabelFormat("%d");
136                break;
137            case 'M':
138                if (mMonthColumn != null) {
139                    throw new IllegalArgumentException("datePicker format error");
140                }
141                columns.add(mMonthColumn = new PickerColumn());
142                mMonthColumn.setValueStaticLabels(mConstant.months);
143                mColMonthIndex = i;
144                break;
145            case 'D':
146                if (mDayColumn != null) {
147                    throw new IllegalArgumentException("datePicker format error");
148                }
149                columns.add(mDayColumn = new PickerColumn());
150                mDayColumn.setValueLabelFormat("%02d");
151                mColDayIndex = i;
152                break;
153            default:
154                throw new IllegalArgumentException("datePicker format error");
155            }
156        }
157        setColumns(columns);
158        updateSpinners(false);
159    }
160
161    /**
162     * Get format of showing dates, e.g. 'YMD'.  Default value is from
163     * {@link android.text.format.DateFormat#getDateFormatOrder}.
164     * @return Format of showing dates.
165     */
166    public String getDatePickerFormat() {
167        return mDatePickerFormat;
168    }
169
170    @Override
171    protected String getSeparator() {
172        return mConstant.dateSeparator;
173    }
174
175
176    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
177        if (oldCalendar == null) {
178            return Calendar.getInstance(locale);
179        } else {
180            final long currentTimeMillis = oldCalendar.getTimeInMillis();
181            Calendar newCalendar = Calendar.getInstance(locale);
182            newCalendar.setTimeInMillis(currentTimeMillis);
183            return newCalendar;
184        }
185    }
186
187    private void updateCurrentLocale() {
188        mConstant = new PickerConstant(Locale.getDefault(), getContext().getResources());
189        mTempDate = getCalendarForLocale(mTempDate, mConstant.locale);
190        mMinDate = getCalendarForLocale(mMinDate, mConstant.locale);
191        mMaxDate = getCalendarForLocale(mMaxDate, mConstant.locale);
192        mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale);
193
194        if (mMonthColumn != null) {
195            mMonthColumn.setValueStaticLabels(mConstant.months);
196            updateAdapter(mColMonthIndex);
197        }
198    }
199
200    @Override
201    public void onColumnValueChange(int column, int newVal) {
202        mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
203        // take care of wrapping of days and months to update greater fields
204        int oldVal = getColumnAt(column).getCurrentValue();
205        if (column == mColDayIndex) {
206            mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
207        } else if (column == mColMonthIndex) {
208            mTempDate.add(Calendar.MONTH, newVal - oldVal);
209        } else if (column == mColYearIndex) {
210            mTempDate.add(Calendar.YEAR, newVal - oldVal);
211        } else {
212            throw new IllegalArgumentException();
213        }
214        setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
215                mTempDate.get(Calendar.DAY_OF_MONTH));
216        updateSpinners(false);
217    }
218
219
220    /**
221     * Sets the minimal date supported by this {@link DatePicker} in
222     * milliseconds since January 1, 1970 00:00:00 in
223     * {@link TimeZone#getDefault()} time zone.
224     *
225     * @param minDate The minimal supported date.
226     */
227    public void setMinDate(long minDate) {
228        mTempDate.setTimeInMillis(minDate);
229        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
230                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
231            return;
232        }
233        mMinDate.setTimeInMillis(minDate);
234        if (mCurrentDate.before(mMinDate)) {
235            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
236        }
237        updateSpinners(false);
238    }
239
240
241    /**
242     * Gets the minimal date supported by this {@link DatePicker} in
243     * milliseconds since January 1, 1970 00:00:00 in
244     * {@link TimeZone#getDefault()} time zone.
245     * <p>
246     * Note: The default minimal date is 01/01/1900.
247     * <p>
248     *
249     * @return The minimal supported date.
250     */
251    public Calendar getMinDate() {
252        final Calendar minDate = Calendar.getInstance();
253        minDate.setTimeInMillis(mMinDate.getTimeInMillis());
254        return minDate;
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 Calendar getMaxDate() {
288        final Calendar maxDate = Calendar.getInstance();
289        maxDate.setTimeInMillis(mMaxDate.getTimeInMillis());
290        return maxDate;
291    }
292
293    private void setDate(int year, int month, int dayOfMonth) {
294        mCurrentDate.set(year, month, dayOfMonth);
295        if (mCurrentDate.before(mMinDate)) {
296            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
297        } else if (mCurrentDate.after(mMaxDate)) {
298            mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
299        }
300    }
301
302    /**
303     * Update the current date.
304     *
305     * @param year The year.
306     * @param month The month which is <strong>starting from zero</strong>.
307     * @param dayOfMonth The day of the month.
308     * @param animation True to run animation to scroll the column.
309     */
310    public void updateDate(int year, int month, int dayOfMonth, boolean animation) {
311        if (!isNewDate(year, month, dayOfMonth)) {
312            return;
313        }
314        setDate(year, month, dayOfMonth);
315        updateSpinners(animation);
316    }
317
318    private boolean isNewDate(int year, int month, int dayOfMonth) {
319        return (mCurrentDate.get(Calendar.YEAR) != year
320                || mCurrentDate.get(Calendar.MONTH) != dayOfMonth
321                || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
322    }
323
324    private void updateSpinners(boolean animation) {
325        // set the spinner ranges respecting the min and max dates
326        boolean dayRangeChanged = false;
327        boolean monthRangeChanged = false;
328        if (mCurrentDate.equals(mMinDate)) {
329            if (mDayColumn != null) {
330                dayRangeChanged |= mDayColumn.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
331                dayRangeChanged |= mDayColumn
332                        .setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
333            }
334            if (mMonthColumn != null) {
335                monthRangeChanged |= mMonthColumn.setMinValue(mCurrentDate.get(Calendar.MONTH));
336                monthRangeChanged |=
337                        mMonthColumn.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
338            }
339        } else if (mCurrentDate.equals(mMaxDate)) {
340            if (mDayColumn != null) {
341                dayRangeChanged |= mDayColumn
342                        .setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
343                dayRangeChanged |= mDayColumn.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
344            }
345            if (mMonthColumn != null) {
346                monthRangeChanged |=
347                        mMonthColumn.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
348                monthRangeChanged |= mMonthColumn.setMaxValue(mCurrentDate.get(Calendar.MONTH));
349            }
350        } else {
351            if (mDayColumn != null) {
352                dayRangeChanged |= mDayColumn
353                        .setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
354                dayRangeChanged |= mDayColumn
355                        .setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
356            }
357            if (mMonthColumn != null) {
358                monthRangeChanged |=
359                        mMonthColumn.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
360                monthRangeChanged |=
361                        mMonthColumn.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
362            }
363        }
364
365        // year spinner range does not change based on the current date
366        boolean yearRangeChanged = false;
367        if (mYearColumn != null) {
368            yearRangeChanged |= mYearColumn.setMinValue(mMinDate.get(Calendar.YEAR));
369            yearRangeChanged |= mYearColumn.setMaxValue(mMaxDate.get(Calendar.YEAR));
370        }
371
372        if (dayRangeChanged) {
373            updateAdapter(mColDayIndex);
374        }
375        if (monthRangeChanged) {
376            updateAdapter(mColMonthIndex);
377        }
378        if (yearRangeChanged) {
379            updateAdapter(mColYearIndex);
380        }
381        // set the spinner values
382        if (mYearColumn != null) {
383            updateValue(mColYearIndex, mCurrentDate.get(Calendar.YEAR), animation);
384        }
385        if (mMonthColumn != null) {
386            updateValue(mColMonthIndex, mCurrentDate.get(Calendar.MONTH), animation);
387        }
388        if (mDayColumn != null) {
389            updateValue(mColDayIndex, mCurrentDate.get(Calendar.DAY_OF_MONTH), animation);
390        }
391
392    }
393
394}