• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Parcelable;
22 import android.text.format.DateFormat;
23 import android.text.format.DateUtils;
24 import android.util.AttributeSet;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.accessibility.AccessibilityEvent;
29 import android.view.inputmethod.EditorInfo;
30 import android.view.inputmethod.InputMethodManager;
31 import com.android.internal.R;
32 
33 import java.util.Calendar;
34 
35 import libcore.icu.LocaleData;
36 
37 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
38 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
39 
40 /**
41  * A delegate implementing the basic spinner-based TimePicker.
42  */
43 class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate {
44     private static final boolean DEFAULT_ENABLED_STATE = true;
45     private static final int HOURS_IN_HALF_DAY = 12;
46 
47     private final NumberPicker mHourSpinner;
48     private final NumberPicker mMinuteSpinner;
49     private final NumberPicker mAmPmSpinner;
50     private final EditText mHourSpinnerInput;
51     private final EditText mMinuteSpinnerInput;
52     private final EditText mAmPmSpinnerInput;
53     private final TextView mDivider;
54 
55     // Note that the legacy implementation of the TimePicker is
56     // using a button for toggling between AM/PM while the new
57     // version uses a NumberPicker spinner. Therefore the code
58     // accommodates these two cases to be backwards compatible.
59     private final Button mAmPmButton;
60 
61     private final String[] mAmPmStrings;
62 
63     private final Calendar mTempCalendar;
64 
65     private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
66     private boolean mHourWithTwoDigit;
67     private char mHourFormat;
68 
69     private boolean mIs24HourView;
70     private boolean mIsAm;
71 
TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)72     public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
73             int defStyleAttr, int defStyleRes) {
74         super(delegator, context);
75 
76         // process style attributes
77         final TypedArray a = mContext.obtainStyledAttributes(
78                 attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
79         final int layoutResourceId = a.getResourceId(
80                 R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
81         a.recycle();
82 
83         final LayoutInflater inflater = LayoutInflater.from(mContext);
84         inflater.inflate(layoutResourceId, mDelegator, true);
85 
86         // hour
87         mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour);
88         mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
89             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
90                 updateInputState();
91                 if (!is24Hour()) {
92                     if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
93                             (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
94                         mIsAm = !mIsAm;
95                         updateAmPmControl();
96                     }
97                 }
98                 onTimeChanged();
99             }
100         });
101         mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
102         mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
103 
104         // divider (only for the new widget style)
105         mDivider = (TextView) mDelegator.findViewById(R.id.divider);
106         if (mDivider != null) {
107             setDividerText();
108         }
109 
110         // minute
111         mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute);
112         mMinuteSpinner.setMinValue(0);
113         mMinuteSpinner.setMaxValue(59);
114         mMinuteSpinner.setOnLongPressUpdateInterval(100);
115         mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
116         mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
117             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
118                 updateInputState();
119                 int minValue = mMinuteSpinner.getMinValue();
120                 int maxValue = mMinuteSpinner.getMaxValue();
121                 if (oldVal == maxValue && newVal == minValue) {
122                     int newHour = mHourSpinner.getValue() + 1;
123                     if (!is24Hour() && newHour == HOURS_IN_HALF_DAY) {
124                         mIsAm = !mIsAm;
125                         updateAmPmControl();
126                     }
127                     mHourSpinner.setValue(newHour);
128                 } else if (oldVal == minValue && newVal == maxValue) {
129                     int newHour = mHourSpinner.getValue() - 1;
130                     if (!is24Hour() && newHour == HOURS_IN_HALF_DAY - 1) {
131                         mIsAm = !mIsAm;
132                         updateAmPmControl();
133                     }
134                     mHourSpinner.setValue(newHour);
135                 }
136                 onTimeChanged();
137             }
138         });
139         mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
140         mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
141 
142         // Get the localized am/pm strings and use them in the spinner.
143         mAmPmStrings = getAmPmStrings(context);
144 
145         // am/pm
146         final View amPmView = mDelegator.findViewById(R.id.amPm);
147         if (amPmView instanceof Button) {
148             mAmPmSpinner = null;
149             mAmPmSpinnerInput = null;
150             mAmPmButton = (Button) amPmView;
151             mAmPmButton.setOnClickListener(new View.OnClickListener() {
152                 public void onClick(View button) {
153                     button.requestFocus();
154                     mIsAm = !mIsAm;
155                     updateAmPmControl();
156                     onTimeChanged();
157                 }
158             });
159         } else {
160             mAmPmButton = null;
161             mAmPmSpinner = (NumberPicker) amPmView;
162             mAmPmSpinner.setMinValue(0);
163             mAmPmSpinner.setMaxValue(1);
164             mAmPmSpinner.setDisplayedValues(mAmPmStrings);
165             mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
166                 public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
167                     updateInputState();
168                     picker.requestFocus();
169                     mIsAm = !mIsAm;
170                     updateAmPmControl();
171                     onTimeChanged();
172                 }
173             });
174             mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
175             mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
176         }
177 
178         if (isAmPmAtStart()) {
179             // Move the am/pm view to the beginning
180             ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout);
181             amPmParent.removeView(amPmView);
182             amPmParent.addView(amPmView, 0);
183             // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
184             // for example and not for Holo Theme)
185             ViewGroup.MarginLayoutParams lp =
186                     (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
187             final int startMargin = lp.getMarginStart();
188             final int endMargin = lp.getMarginEnd();
189             if (startMargin != endMargin) {
190                 lp.setMarginStart(endMargin);
191                 lp.setMarginEnd(startMargin);
192             }
193         }
194 
195         getHourFormatData();
196 
197         // update controls to initial state
198         updateHourControl();
199         updateMinuteControl();
200         updateAmPmControl();
201 
202         // set to current time
203         mTempCalendar = Calendar.getInstance(mLocale);
204         setHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
205         setMinute(mTempCalendar.get(Calendar.MINUTE));
206 
207         if (!isEnabled()) {
208             setEnabled(false);
209         }
210 
211         // set the content descriptions
212         setContentDescriptions();
213 
214         // If not explicitly specified this view is important for accessibility.
215         if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
216             mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
217         }
218     }
219 
getHourFormatData()220     private void getHourFormatData() {
221         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
222                 (mIs24HourView) ? "Hm" : "hm");
223         final int lengthPattern = bestDateTimePattern.length();
224         mHourWithTwoDigit = false;
225         char hourFormat = '\0';
226         // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
227         // the hour format that we found.
228         for (int i = 0; i < lengthPattern; i++) {
229             final char c = bestDateTimePattern.charAt(i);
230             if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
231                 mHourFormat = c;
232                 if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
233                     mHourWithTwoDigit = true;
234                 }
235                 break;
236             }
237         }
238     }
239 
isAmPmAtStart()240     private boolean isAmPmAtStart() {
241         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
242                 "hm" /* skeleton */);
243 
244         return bestDateTimePattern.startsWith("a");
245     }
246 
247     /**
248      * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
249      *
250      * See http://unicode.org/cldr/trac/browser/trunk/common/main
251      *
252      * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
253      * separator as the character which is just after the hour marker in the returned pattern.
254      */
setDividerText()255     private void setDividerText() {
256         final String skeleton = (mIs24HourView) ? "Hm" : "hm";
257         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
258                 skeleton);
259         final String separatorText;
260         int hourIndex = bestDateTimePattern.lastIndexOf('H');
261         if (hourIndex == -1) {
262             hourIndex = bestDateTimePattern.lastIndexOf('h');
263         }
264         if (hourIndex == -1) {
265             // Default case
266             separatorText = ":";
267         } else {
268             int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
269             if  (minuteIndex == -1) {
270                 separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
271             } else {
272                 separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
273             }
274         }
275         mDivider.setText(separatorText);
276     }
277 
278     @Override
setHour(int hour)279     public void setHour(int hour) {
280         setCurrentHour(hour, true);
281     }
282 
setCurrentHour(int currentHour, boolean notifyTimeChanged)283     private void setCurrentHour(int currentHour, boolean notifyTimeChanged) {
284         // why was Integer used in the first place?
285         if (currentHour == getHour()) {
286             return;
287         }
288         if (!is24Hour()) {
289             // convert [0,23] ordinal to wall clock display
290             if (currentHour >= HOURS_IN_HALF_DAY) {
291                 mIsAm = false;
292                 if (currentHour > HOURS_IN_HALF_DAY) {
293                     currentHour = currentHour - HOURS_IN_HALF_DAY;
294                 }
295             } else {
296                 mIsAm = true;
297                 if (currentHour == 0) {
298                     currentHour = HOURS_IN_HALF_DAY;
299                 }
300             }
301             updateAmPmControl();
302         }
303         mHourSpinner.setValue(currentHour);
304         if (notifyTimeChanged) {
305             onTimeChanged();
306         }
307     }
308 
309     @Override
getHour()310     public int getHour() {
311         int currentHour = mHourSpinner.getValue();
312         if (is24Hour()) {
313             return currentHour;
314         } else if (mIsAm) {
315             return currentHour % HOURS_IN_HALF_DAY;
316         } else {
317             return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
318         }
319     }
320 
321     @Override
setMinute(int minute)322     public void setMinute(int minute) {
323         if (minute == getMinute()) {
324             return;
325         }
326         mMinuteSpinner.setValue(minute);
327         onTimeChanged();
328     }
329 
330     @Override
getMinute()331     public int getMinute() {
332         return mMinuteSpinner.getValue();
333     }
334 
setIs24Hour(boolean is24Hour)335     public void setIs24Hour(boolean is24Hour) {
336         if (mIs24HourView == is24Hour) {
337             return;
338         }
339         // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
340         int currentHour = getHour();
341         // Order is important here.
342         mIs24HourView = is24Hour;
343         getHourFormatData();
344         updateHourControl();
345         // set value after spinner range is updated
346         setCurrentHour(currentHour, false);
347         updateMinuteControl();
348         updateAmPmControl();
349     }
350 
351     @Override
is24Hour()352     public boolean is24Hour() {
353         return mIs24HourView;
354     }
355 
356     @Override
setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener)357     public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) {
358         mOnTimeChangedListener = onTimeChangedListener;
359     }
360 
361     @Override
setEnabled(boolean enabled)362     public void setEnabled(boolean enabled) {
363         mMinuteSpinner.setEnabled(enabled);
364         if (mDivider != null) {
365             mDivider.setEnabled(enabled);
366         }
367         mHourSpinner.setEnabled(enabled);
368         if (mAmPmSpinner != null) {
369             mAmPmSpinner.setEnabled(enabled);
370         } else {
371             mAmPmButton.setEnabled(enabled);
372         }
373         mIsEnabled = enabled;
374     }
375 
376     @Override
isEnabled()377     public boolean isEnabled() {
378         return mIsEnabled;
379     }
380 
381     @Override
getBaseline()382     public int getBaseline() {
383         return mHourSpinner.getBaseline();
384     }
385 
386     @Override
onSaveInstanceState(Parcelable superState)387     public Parcelable onSaveInstanceState(Parcelable superState) {
388         return new SavedState(superState, getHour(), getMinute(), is24Hour());
389     }
390 
391     @Override
onRestoreInstanceState(Parcelable state)392     public void onRestoreInstanceState(Parcelable state) {
393         if (state instanceof SavedState) {
394             final SavedState ss = (SavedState) state;
395             setHour(ss.getHour());
396             setMinute(ss.getMinute());
397         }
398     }
399 
400     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)401     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
402         onPopulateAccessibilityEvent(event);
403         return true;
404     }
405 
406     @Override
onPopulateAccessibilityEvent(AccessibilityEvent event)407     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
408         int flags = DateUtils.FORMAT_SHOW_TIME;
409         if (mIs24HourView) {
410             flags |= DateUtils.FORMAT_24HOUR;
411         } else {
412             flags |= DateUtils.FORMAT_12HOUR;
413         }
414         mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour());
415         mTempCalendar.set(Calendar.MINUTE, getMinute());
416         String selectedDateUtterance = DateUtils.formatDateTime(mContext,
417                 mTempCalendar.getTimeInMillis(), flags);
418         event.getText().add(selectedDateUtterance);
419     }
420 
updateInputState()421     private void updateInputState() {
422         // Make sure that if the user changes the value and the IME is active
423         // for one of the inputs if this widget, the IME is closed. If the user
424         // changed the value via the IME and there is a next input the IME will
425         // be shown, otherwise the user chose another means of changing the
426         // value and having the IME up makes no sense.
427         InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
428         if (inputMethodManager != null) {
429             if (inputMethodManager.isActive(mHourSpinnerInput)) {
430                 mHourSpinnerInput.clearFocus();
431                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
432             } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
433                 mMinuteSpinnerInput.clearFocus();
434                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
435             } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
436                 mAmPmSpinnerInput.clearFocus();
437                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
438             }
439         }
440     }
441 
updateAmPmControl()442     private void updateAmPmControl() {
443         if (is24Hour()) {
444             if (mAmPmSpinner != null) {
445                 mAmPmSpinner.setVisibility(View.GONE);
446             } else {
447                 mAmPmButton.setVisibility(View.GONE);
448             }
449         } else {
450             int index = mIsAm ? Calendar.AM : Calendar.PM;
451             if (mAmPmSpinner != null) {
452                 mAmPmSpinner.setValue(index);
453                 mAmPmSpinner.setVisibility(View.VISIBLE);
454             } else {
455                 mAmPmButton.setText(mAmPmStrings[index]);
456                 mAmPmButton.setVisibility(View.VISIBLE);
457             }
458         }
459         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
460     }
461 
onTimeChanged()462     private void onTimeChanged() {
463         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
464         if (mOnTimeChangedListener != null) {
465             mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(),
466                     getMinute());
467         }
468     }
469 
updateHourControl()470     private void updateHourControl() {
471         if (is24Hour()) {
472             // 'k' means 1-24 hour
473             if (mHourFormat == 'k') {
474                 mHourSpinner.setMinValue(1);
475                 mHourSpinner.setMaxValue(24);
476             } else {
477                 mHourSpinner.setMinValue(0);
478                 mHourSpinner.setMaxValue(23);
479             }
480         } else {
481             // 'K' means 0-11 hour
482             if (mHourFormat == 'K') {
483                 mHourSpinner.setMinValue(0);
484                 mHourSpinner.setMaxValue(11);
485             } else {
486                 mHourSpinner.setMinValue(1);
487                 mHourSpinner.setMaxValue(12);
488             }
489         }
490         mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
491     }
492 
updateMinuteControl()493     private void updateMinuteControl() {
494         if (is24Hour()) {
495             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
496         } else {
497             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
498         }
499     }
500 
setContentDescriptions()501     private void setContentDescriptions() {
502         // Minute
503         trySetContentDescription(mMinuteSpinner, R.id.increment,
504                 R.string.time_picker_increment_minute_button);
505         trySetContentDescription(mMinuteSpinner, R.id.decrement,
506                 R.string.time_picker_decrement_minute_button);
507         // Hour
508         trySetContentDescription(mHourSpinner, R.id.increment,
509                 R.string.time_picker_increment_hour_button);
510         trySetContentDescription(mHourSpinner, R.id.decrement,
511                 R.string.time_picker_decrement_hour_button);
512         // AM/PM
513         if (mAmPmSpinner != null) {
514             trySetContentDescription(mAmPmSpinner, R.id.increment,
515                     R.string.time_picker_increment_set_pm_button);
516             trySetContentDescription(mAmPmSpinner, R.id.decrement,
517                     R.string.time_picker_decrement_set_am_button);
518         }
519     }
520 
trySetContentDescription(View root, int viewId, int contDescResId)521     private void trySetContentDescription(View root, int viewId, int contDescResId) {
522         View target = root.findViewById(viewId);
523         if (target != null) {
524             target.setContentDescription(mContext.getString(contDescResId));
525         }
526     }
527 
getAmPmStrings(Context context)528     public static String[] getAmPmStrings(Context context) {
529         String[] result = new String[2];
530         LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
531         result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
532         result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
533         return result;
534     }
535 }
536