1 | // Copyright 2013 The Chromium Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | package org.chromium.content.browser.input; |
6 | |
7 | import android.content.Context; |
8 | import android.view.LayoutInflater; |
9 | import android.widget.NumberPicker; |
10 | import android.widget.NumberPicker.OnValueChangeListener; |
11 | import android.text.format.DateUtils; |
12 | import android.view.accessibility.AccessibilityEvent; |
13 | import android.widget.FrameLayout; |
14 | import android.widget.NumberPicker; |
15 | |
16 | import java.util.Calendar; |
17 | |
18 | import org.chromium.content.R; |
19 | |
20 | // This class is heavily based on android.widget.DatePicker. |
21 | public abstract class TwoFieldDatePicker extends FrameLayout { |
22 | |
23 | private NumberPicker mPositionInYearSpinner; |
24 | |
25 | private NumberPicker mYearSpinner; |
26 | |
27 | private OnMonthOrWeekChangedListener mMonthOrWeekChangedListener; |
28 | |
29 | // It'd be nice to use android.text.Time like in other Dialogs but |
30 | // it suffers from the 2038 effect so it would prevent us from |
31 | // having dates over 2038. |
32 | private Calendar mMinDate; |
33 | |
34 | private Calendar mMaxDate; |
35 | |
36 | private Calendar mCurrentDate; |
37 | |
38 | /** |
39 | * The callback used to indicate the user changes\d the date. |
40 | */ |
41 | public interface OnMonthOrWeekChangedListener { |
42 | |
43 | /** |
44 | * Called upon a date change. |
45 | * |
46 | * @param view The view associated with this listener. |
47 | * @param year The year that was set. |
48 | * @param positionInYear The month or week in year. |
49 | */ |
50 | void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear); |
51 | } |
52 | |
53 | public TwoFieldDatePicker(Context context, long minValue, long maxValue) { |
54 | super(context, null, android.R.attr.datePickerStyle); |
55 | |
56 | LayoutInflater inflater = (LayoutInflater) context |
57 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
58 | inflater.inflate(R.layout.two_field_date_picker, this, true); |
59 | |
60 | OnValueChangeListener onChangeListener = new OnValueChangeListener() { |
61 | @Override |
62 | public void onValueChange(NumberPicker picker, int oldVal, int newVal) { |
63 | int year = getYear(); |
64 | int positionInYear = getPositionInYear(); |
65 | // take care of wrapping of days and months to update greater fields |
66 | if (picker == mPositionInYearSpinner) { |
67 | positionInYear = newVal; |
68 | if (oldVal == picker.getMaxValue() && newVal == picker.getMinValue()) { |
69 | year += 1; |
70 | } else if (oldVal == picker.getMinValue() && newVal == picker.getMaxValue()) { |
71 | year -=1; |
72 | } |
73 | } else if (picker == mYearSpinner) { |
74 | year = newVal; |
75 | } else { |
76 | throw new IllegalArgumentException(); |
77 | } |
78 | |
79 | // now set the date to the adjusted one |
80 | setCurrentDate(year, positionInYear); |
81 | updateSpinners(); |
82 | notifyDateChanged(); |
83 | } |
84 | }; |
85 | |
86 | mCurrentDate = Calendar.getInstance(); |
87 | if (minValue >= maxValue) { |
88 | mMinDate = Calendar.getInstance(); |
89 | mMinDate.set(0, 0, 1); |
90 | mMaxDate = Calendar.getInstance(); |
91 | mMaxDate.set(9999, 0, 1); |
92 | } else { |
93 | mMinDate = createDateFromValue(minValue); |
94 | mMaxDate = createDateFromValue(maxValue); |
95 | } |
96 | |
97 | // month |
98 | mPositionInYearSpinner = (NumberPicker) findViewById(R.id.position_in_year); |
99 | mPositionInYearSpinner.setOnLongPressUpdateInterval(200); |
100 | mPositionInYearSpinner.setOnValueChangedListener(onChangeListener); |
101 | |
102 | // year |
103 | mYearSpinner = (NumberPicker) findViewById(R.id.year); |
104 | mYearSpinner.setOnLongPressUpdateInterval(100); |
105 | mYearSpinner.setOnValueChangedListener(onChangeListener); |
106 | } |
107 | |
108 | /** |
109 | * Initialize the state. If the provided values designate an inconsistent |
110 | * date the values are normalized before updating the spinners. |
111 | * |
112 | * @param year The initial year. |
113 | * @param positionInYear The initial month <strong>starting from zero</strong> or week in year. |
114 | * @param onMonthChangedListener How user is notified date is changed by |
115 | * user, can be null. |
116 | */ |
117 | public void init(int year, int positionInYear, |
118 | OnMonthOrWeekChangedListener onMonthOrWeekChangedListener) { |
119 | setCurrentDate(year, positionInYear); |
120 | updateSpinners(); |
121 | mMonthOrWeekChangedListener = onMonthOrWeekChangedListener; |
122 | } |
123 | |
124 | public boolean isNewDate(int year, int positionInYear) { |
125 | return (getYear() != year || getPositionInYear() != positionInYear); |
126 | } |
127 | |
128 | /** |
129 | * Subclasses know the semantics of @value, and need to return |
130 | * a Calendar corresponding to it. |
131 | */ |
132 | protected abstract Calendar createDateFromValue(long value); |
133 | |
134 | /** |
135 | * Updates the current date. |
136 | * |
137 | * @param year The year. |
138 | * @param positionInYear The month or week in year. |
139 | */ |
140 | public void updateDate(int year, int positionInYear) { |
141 | if (!isNewDate(year, positionInYear)) { |
142 | return; |
143 | } |
144 | setCurrentDate(year, positionInYear); |
145 | updateSpinners(); |
146 | notifyDateChanged(); |
147 | } |
148 | |
149 | /** |
150 | * Subclasses know the semantics of @positionInYear, and need to update @mCurrentDate to the |
151 | * appropriate date. |
152 | */ |
153 | protected abstract void setCurrentDate(int year, int positionInYear); |
154 | |
155 | protected void setCurrentDate(Calendar date) { |
156 | mCurrentDate = date; |
157 | } |
158 | |
159 | @Override |
160 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
161 | onPopulateAccessibilityEvent(event); |
162 | return true; |
163 | } |
164 | |
165 | @Override |
166 | public void onPopulateAccessibilityEvent(AccessibilityEvent event) { |
167 | super.onPopulateAccessibilityEvent(event); |
168 | |
169 | final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; |
170 | String selectedDateUtterance = DateUtils.formatDateTime(getContext(), |
171 | mCurrentDate.getTimeInMillis(), flags); |
172 | event.getText().add(selectedDateUtterance); |
173 | } |
174 | |
175 | /** |
176 | * @return The selected year. |
177 | */ |
178 | public int getYear() { |
179 | return mCurrentDate.get(Calendar.YEAR); |
180 | } |
181 | |
182 | /** |
183 | * @return The selected month or week. |
184 | */ |
185 | public abstract int getPositionInYear(); |
186 | |
187 | protected abstract int getMaxYear(); |
188 | |
189 | protected abstract int getMinYear(); |
190 | |
191 | protected abstract int getMaxPositionInYear(); |
192 | |
193 | protected abstract int getMinPositionInYear(); |
194 | |
195 | protected Calendar getMaxDate() { |
196 | return mMaxDate; |
197 | } |
198 | |
199 | protected Calendar getMinDate() { |
200 | return mMinDate; |
201 | } |
202 | |
203 | protected Calendar getCurrentDate() { |
204 | return mCurrentDate; |
205 | } |
206 | |
207 | protected NumberPicker getPositionInYearSpinner() { |
208 | return mPositionInYearSpinner; |
209 | } |
210 | |
211 | protected NumberPicker getYearSpinner() { |
212 | return mYearSpinner; |
213 | } |
214 | |
215 | /** |
216 | * This method should be subclassed to update the spinners based on mCurrentDate. |
217 | */ |
218 | protected void updateSpinners() { |
219 | mPositionInYearSpinner.setDisplayedValues(null); |
220 | |
221 | // set the spinner ranges respecting the min and max dates |
222 | mPositionInYearSpinner.setMinValue(getMinPositionInYear()); |
223 | mPositionInYearSpinner.setMaxValue(getMaxPositionInYear()); |
224 | mPositionInYearSpinner.setWrapSelectorWheel( |
225 | !mCurrentDate.equals(mMinDate) && !mCurrentDate.equals(mMaxDate)); |
226 | |
227 | // year spinner range does not change based on the current date |
228 | mYearSpinner.setMinValue(getMinYear()); |
229 | mYearSpinner.setMaxValue(getMaxYear()); |
230 | mYearSpinner.setWrapSelectorWheel(false); |
231 | |
232 | // set the spinner values |
233 | mYearSpinner.setValue(getYear()); |
234 | mPositionInYearSpinner.setValue(getPositionInYear()); |
235 | } |
236 | |
237 | /** |
238 | * Notifies the listener, if such, for a change in the selected date. |
239 | */ |
240 | protected void notifyDateChanged() { |
241 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); |
242 | if (mMonthOrWeekChangedListener != null) { |
243 | mMonthOrWeekChangedListener.onMonthOrWeekChanged(this, getYear(), getPositionInYear()); |
244 | } |
245 | } |
246 | } |