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