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