• 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
setHour(int hour)287     public void setHour(int hour) {
288         setCurrentHour(hour, true);
289     }
290 
setCurrentHour(int currentHour, boolean notifyTimeChanged)291     private void setCurrentHour(int currentHour, boolean notifyTimeChanged) {
292         // why was Integer used in the first place?
293         if (currentHour == getHour()) {
294             return;
295         }
296         if (!is24Hour()) {
297             // convert [0,23] ordinal to wall clock display
298             if (currentHour >= HOURS_IN_HALF_DAY) {
299                 mIsAm = false;
300                 if (currentHour > HOURS_IN_HALF_DAY) {
301                     currentHour = currentHour - HOURS_IN_HALF_DAY;
302                 }
303             } else {
304                 mIsAm = true;
305                 if (currentHour == 0) {
306                     currentHour = HOURS_IN_HALF_DAY;
307                 }
308             }
309             updateAmPmControl();
310         }
311         mHourSpinner.setValue(currentHour);
312         if (notifyTimeChanged) {
313             onTimeChanged();
314         }
315     }
316 
317     @Override
getHour()318     public int getHour() {
319         int currentHour = mHourSpinner.getValue();
320         if (is24Hour()) {
321             return currentHour;
322         } else if (mIsAm) {
323             return currentHour % HOURS_IN_HALF_DAY;
324         } else {
325             return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
326         }
327     }
328 
329     @Override
setMinute(int minute)330     public void setMinute(int minute) {
331         if (minute == getMinute()) {
332             return;
333         }
334         mMinuteSpinner.setValue(minute);
335         onTimeChanged();
336     }
337 
338     @Override
getMinute()339     public int getMinute() {
340         return mMinuteSpinner.getValue();
341     }
342 
setIs24Hour(boolean is24Hour)343     public void setIs24Hour(boolean is24Hour) {
344         if (mIs24HourView == is24Hour) {
345             return;
346         }
347         // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
348         int currentHour = getHour();
349         // Order is important here.
350         mIs24HourView = is24Hour;
351         getHourFormatData();
352         updateHourControl();
353         // set value after spinner range is updated
354         setCurrentHour(currentHour, false);
355         updateMinuteControl();
356         updateAmPmControl();
357     }
358 
359     @Override
is24Hour()360     public boolean is24Hour() {
361         return mIs24HourView;
362     }
363 
364     @Override
setEnabled(boolean enabled)365     public void setEnabled(boolean enabled) {
366         mMinuteSpinner.setEnabled(enabled);
367         if (mDivider != null) {
368             mDivider.setEnabled(enabled);
369         }
370         mHourSpinner.setEnabled(enabled);
371         if (mAmPmSpinner != null) {
372             mAmPmSpinner.setEnabled(enabled);
373         } else {
374             mAmPmButton.setEnabled(enabled);
375         }
376         mIsEnabled = enabled;
377     }
378 
379     @Override
isEnabled()380     public boolean isEnabled() {
381         return mIsEnabled;
382     }
383 
384     @Override
getBaseline()385     public int getBaseline() {
386         return mHourSpinner.getBaseline();
387     }
388 
389     @Override
onSaveInstanceState(Parcelable superState)390     public Parcelable onSaveInstanceState(Parcelable superState) {
391         return new SavedState(superState, getHour(), getMinute(), is24Hour());
392     }
393 
394     @Override
onRestoreInstanceState(Parcelable state)395     public void onRestoreInstanceState(Parcelable state) {
396         if (state instanceof SavedState) {
397             final SavedState ss = (SavedState) state;
398             setHour(ss.getHour());
399             setMinute(ss.getMinute());
400         }
401     }
402 
403     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)404     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
405         onPopulateAccessibilityEvent(event);
406         return true;
407     }
408 
409     @Override
onPopulateAccessibilityEvent(AccessibilityEvent event)410     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
411         int flags = DateUtils.FORMAT_SHOW_TIME;
412         if (mIs24HourView) {
413             flags |= DateUtils.FORMAT_24HOUR;
414         } else {
415             flags |= DateUtils.FORMAT_12HOUR;
416         }
417         mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour());
418         mTempCalendar.set(Calendar.MINUTE, getMinute());
419         String selectedDateUtterance = DateUtils.formatDateTime(mContext,
420                 mTempCalendar.getTimeInMillis(), flags);
421         event.getText().add(selectedDateUtterance);
422     }
423 
424     /** @hide */
425     @Override
426     @TestApi
getHourView()427     public View getHourView() {
428         return mHourSpinnerInput;
429     }
430 
431     /** @hide */
432     @Override
433     @TestApi
getMinuteView()434     public View getMinuteView() {
435         return mMinuteSpinnerInput;
436     }
437 
438     /** @hide */
439     @Override
440     @TestApi
getAmView()441     public View getAmView() {
442         return mAmPmSpinnerInput;
443     }
444 
445     /** @hide */
446     @Override
447     @TestApi
getPmView()448     public View getPmView() {
449         return mAmPmSpinnerInput;
450     }
451 
updateInputState()452     private void updateInputState() {
453         // Make sure that if the user changes the value and the IME is active
454         // for one of the inputs if this widget, the IME is closed. If the user
455         // changed the value via the IME and there is a next input the IME will
456         // be shown, otherwise the user chose another means of changing the
457         // value and having the IME up makes no sense.
458         InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
459         if (inputMethodManager != null) {
460             if (inputMethodManager.isActive(mHourSpinnerInput)) {
461                 mHourSpinnerInput.clearFocus();
462                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
463             } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
464                 mMinuteSpinnerInput.clearFocus();
465                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
466             } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
467                 mAmPmSpinnerInput.clearFocus();
468                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
469             }
470         }
471     }
472 
updateAmPmControl()473     private void updateAmPmControl() {
474         if (is24Hour()) {
475             if (mAmPmSpinner != null) {
476                 mAmPmSpinner.setVisibility(View.GONE);
477             } else {
478                 mAmPmButton.setVisibility(View.GONE);
479             }
480         } else {
481             int index = mIsAm ? Calendar.AM : Calendar.PM;
482             if (mAmPmSpinner != null) {
483                 mAmPmSpinner.setValue(index);
484                 mAmPmSpinner.setVisibility(View.VISIBLE);
485             } else {
486                 mAmPmButton.setText(mAmPmStrings[index]);
487                 mAmPmButton.setVisibility(View.VISIBLE);
488             }
489         }
490         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
491     }
492 
onTimeChanged()493     private void onTimeChanged() {
494         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
495         if (mOnTimeChangedListener != null) {
496             mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(),
497                     getMinute());
498         }
499         if (mAutoFillChangeListener != null) {
500             mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute());
501         }
502     }
503 
updateHourControl()504     private void updateHourControl() {
505         if (is24Hour()) {
506             // 'k' means 1-24 hour
507             if (mHourFormat == 'k') {
508                 mHourSpinner.setMinValue(1);
509                 mHourSpinner.setMaxValue(24);
510             } else {
511                 mHourSpinner.setMinValue(0);
512                 mHourSpinner.setMaxValue(23);
513             }
514         } else {
515             // 'K' means 0-11 hour
516             if (mHourFormat == 'K') {
517                 mHourSpinner.setMinValue(0);
518                 mHourSpinner.setMaxValue(11);
519             } else {
520                 mHourSpinner.setMinValue(1);
521                 mHourSpinner.setMaxValue(12);
522             }
523         }
524         mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
525     }
526 
updateMinuteControl()527     private void updateMinuteControl() {
528         if (is24Hour()) {
529             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
530         } else {
531             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
532         }
533     }
534 
setContentDescriptions()535     private void setContentDescriptions() {
536         // Minute
537         trySetContentDescription(mMinuteSpinner, R.id.increment,
538                 R.string.time_picker_increment_minute_button);
539         trySetContentDescription(mMinuteSpinner, R.id.decrement,
540                 R.string.time_picker_decrement_minute_button);
541         // Hour
542         trySetContentDescription(mHourSpinner, R.id.increment,
543                 R.string.time_picker_increment_hour_button);
544         trySetContentDescription(mHourSpinner, R.id.decrement,
545                 R.string.time_picker_decrement_hour_button);
546         // AM/PM
547         if (mAmPmSpinner != null) {
548             trySetContentDescription(mAmPmSpinner, R.id.increment,
549                     R.string.time_picker_increment_set_pm_button);
550             trySetContentDescription(mAmPmSpinner, R.id.decrement,
551                     R.string.time_picker_decrement_set_am_button);
552         }
553     }
554 
trySetContentDescription(View root, int viewId, int contDescResId)555     private void trySetContentDescription(View root, int viewId, int contDescResId) {
556         View target = root.findViewById(viewId);
557         if (target != null) {
558             target.setContentDescription(mContext.getString(contDescResId));
559         }
560     }
561 
getAmPmStrings(Context context)562     public static String[] getAmPmStrings(Context context) {
563         String[] result = new String[2];
564         LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
565         result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
566         result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
567         return result;
568     }
569 }
570