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