Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.IntRange;
     21 import android.annotation.NonNull;
     22 import android.annotation.TestApi;
     23 import android.annotation.UnsupportedAppUsage;
     24 import android.annotation.Widget;
     25 import android.content.Context;
     26 import android.content.res.TypedArray;
     27 import android.icu.util.Calendar;
     28 import android.os.Parcel;
     29 import android.os.Parcelable;
     30 import android.util.AttributeSet;
     31 import android.util.Log;
     32 import android.util.MathUtils;
     33 import android.view.View;
     34 import android.view.ViewStructure;
     35 import android.view.accessibility.AccessibilityEvent;
     36 import android.view.autofill.AutofillManager;
     37 import android.view.autofill.AutofillValue;
     38 import android.view.inspector.InspectableProperty;
     39 
     40 import com.android.internal.R;
     41 
     42 import libcore.icu.LocaleData;
     43 
     44 import java.lang.annotation.Retention;
     45 import java.lang.annotation.RetentionPolicy;
     46 import java.util.Locale;
     47 
     48 /**
     49  * A widget for selecting the time of day, in either 24-hour or AM/PM mode.
     50  * <p>
     51  * For a dialog using this view, see {@link android.app.TimePickerDialog}. See
     52  * the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
     53  * guide for more information.
     54  *
     55  * @attr ref android.R.styleable#TimePicker_timePickerMode
     56  */
     57 @Widget
     58 public class TimePicker extends FrameLayout {
     59     private static final String LOG_TAG = TimePicker.class.getSimpleName();
     60 
     61     /**
     62      * Presentation mode for the Holo-style time picker that uses a set of
     63      * {@link android.widget.NumberPicker}s.
     64      *
     65      * @see #getMode()
     66      * @hide Visible for testing only.
     67      */
     68     @TestApi
     69     public static final int MODE_SPINNER = 1;
     70 
     71     /**
     72      * Presentation mode for the Material-style time picker that uses a clock
     73      * face.
     74      *
     75      * @see #getMode()
     76      * @hide Visible for testing only.
     77      */
     78     @TestApi
     79     public static final int MODE_CLOCK = 2;
     80 
     81     /** @hide */
     82     @IntDef(prefix = { "MODE_" }, value = {
     83             MODE_SPINNER,
     84             MODE_CLOCK
     85     })
     86     @Retention(RetentionPolicy.SOURCE)
     87     public @interface TimePickerMode {}
     88 
     89     @UnsupportedAppUsage
     90     private final TimePickerDelegate mDelegate;
     91 
     92     @TimePickerMode
     93     private final int mMode;
     94 
     95     /**
     96      * The callback interface used to indicate the time has been adjusted.
     97      */
     98     public interface OnTimeChangedListener {
     99 
    100         /**
    101          * @param view The view associated with this listener.
    102          * @param hourOfDay The current hour.
    103          * @param minute The current minute.
    104          */
    105         void onTimeChanged(TimePicker view, int hourOfDay, int minute);
    106     }
    107 
    108     public TimePicker(Context context) {
    109         this(context, null);
    110     }
    111 
    112     public TimePicker(Context context, AttributeSet attrs) {
    113         this(context, attrs, R.attr.timePickerStyle);
    114     }
    115 
    116     public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
    117         this(context, attrs, defStyleAttr, 0);
    118     }
    119 
    120     public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    121         super(context, attrs, defStyleAttr, defStyleRes);
    122 
    123         // DatePicker is important by default, unless app developer overrode attribute.
    124         if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
    125             setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
    126         }
    127 
    128         final TypedArray a = context.obtainStyledAttributes(
    129                 attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
    130         saveAttributeDataForStyleable(context, R.styleable.TimePicker,
    131                 attrs, a, defStyleAttr, defStyleRes);
    132         final boolean isDialogMode = a.getBoolean(R.styleable.TimePicker_dialogMode, false);
    133         final int requestedMode = a.getInt(R.styleable.TimePicker_timePickerMode, MODE_SPINNER);
    134         a.recycle();
    135 
    136         if (requestedMode == MODE_CLOCK && isDialogMode) {
    137             // You want MODE_CLOCK? YOU CAN'T HANDLE MODE_CLOCK! Well, maybe
    138             // you can depending on your screen size. Let's check...
    139             mMode = context.getResources().getInteger(R.integer.time_picker_mode);
    140         } else {
    141             mMode = requestedMode;
    142         }
    143 
    144         switch (mMode) {
    145             case MODE_CLOCK:
    146                 mDelegate = new TimePickerClockDelegate(
    147                         this, context, attrs, defStyleAttr, defStyleRes);
    148                 break;
    149             case MODE_SPINNER:
    150             default:
    151                 mDelegate = new TimePickerSpinnerDelegate(
    152                         this, context, attrs, defStyleAttr, defStyleRes);
    153                 break;
    154         }
    155         mDelegate.setAutoFillChangeListener((v, h, m) -> {
    156             final AutofillManager afm = context.getSystemService(AutofillManager.class);
    157             if (afm != null) {
    158                 afm.notifyValueChanged(this);
    159             }
    160         });
    161     }
    162 
    163     /**
    164      * @return the picker's presentation mode, one of {@link #MODE_CLOCK} or
    165      *         {@link #MODE_SPINNER}
    166      * @attr ref android.R.styleable#TimePicker_timePickerMode
    167      * @hide Visible for testing only.
    168      */
    169     @TimePickerMode
    170     @TestApi
    171     @InspectableProperty(name = "timePickerMode", enumMapping = {
    172             @InspectableProperty.EnumEntry(name = "clock", value = MODE_CLOCK),
    173             @InspectableProperty.EnumEntry(name = "spinner", value = MODE_SPINNER)
    174     })
    175     public int getMode() {
    176         return mMode;
    177     }
    178 
    179     /**
    180      * Sets the currently selected hour using 24-hour time.
    181      *
    182      * @param hour the hour to set, in the range (0-23)
    183      * @see #getHour()
    184      */
    185     public void setHour(@IntRange(from = 0, to = 23) int hour) {
    186         mDelegate.setHour(MathUtils.constrain(hour, 0, 23));
    187     }
    188 
    189     /**
    190      * Returns the currently selected hour using 24-hour time.
    191      *
    192      * @return the currently selected hour, in the range (0-23)
    193      * @see #setHour(int)
    194      */
    195     @InspectableProperty(hasAttributeId = false)
    196     public int getHour() {
    197         return mDelegate.getHour();
    198     }
    199 
    200     /**
    201      * Sets the currently selected minute.
    202      *
    203      * @param minute the minute to set, in the range (0-59)
    204      * @see #getMinute()
    205      */
    206     public void setMinute(@IntRange(from = 0, to = 59) int minute) {
    207         mDelegate.setMinute(MathUtils.constrain(minute, 0, 59));
    208     }
    209 
    210     /**
    211      * Returns the currently selected minute.
    212      *
    213      * @return the currently selected minute, in the range (0-59)
    214      * @see #setMinute(int)
    215      */
    216     @InspectableProperty(hasAttributeId = false)
    217     public int getMinute() {
    218         return mDelegate.getMinute();
    219     }
    220 
    221     /**
    222      * Sets the currently selected hour using 24-hour time.
    223      *
    224      * @param currentHour the hour to set, in the range (0-23)
    225      * @deprecated Use {@link #setHour(int)}
    226      */
    227     @Deprecated
    228     public void setCurrentHour(@NonNull Integer currentHour) {
    229         setHour(currentHour);
    230     }
    231 
    232     /**
    233      * @return the currently selected hour, in the range (0-23)
    234      * @deprecated Use {@link #getHour()}
    235      */
    236     @NonNull
    237     @Deprecated
    238     public Integer getCurrentHour() {
    239         return getHour();
    240     }
    241 
    242     /**
    243      * Sets the currently selected minute.
    244      *
    245      * @param currentMinute the minute to set, in the range (0-59)
    246      * @deprecated Use {@link #setMinute(int)}
    247      */
    248     @Deprecated
    249     public void setCurrentMinute(@NonNull Integer currentMinute) {
    250         setMinute(currentMinute);
    251     }
    252 
    253     /**
    254      * @return the currently selected minute, in the range (0-59)
    255      * @deprecated Use {@link #getMinute()}
    256      */
    257     @NonNull
    258     @Deprecated
    259     public Integer getCurrentMinute() {
    260         return getMinute();
    261     }
    262 
    263     /**
    264      * Sets whether this widget displays time in 24-hour mode or 12-hour mode
    265      * with an AM/PM picker.
    266      *
    267      * @param is24HourView {@code true} to display in 24-hour mode,
    268      *                     {@code false} for 12-hour mode with AM/PM
    269      * @see #is24HourView()
    270      */
    271     public void setIs24HourView(@NonNull Boolean is24HourView) {
    272         if (is24HourView == null) {
    273             return;
    274         }
    275 
    276         mDelegate.setIs24Hour(is24HourView);
    277     }
    278 
    279     /**
    280      * @return {@code true} if this widget displays time in 24-hour mode,
    281      *         {@code false} otherwise}
    282      * @see #setIs24HourView(Boolean)
    283      */
    284     @InspectableProperty(hasAttributeId = false, name = "24Hour")
    285     public boolean is24HourView() {
    286         return mDelegate.is24Hour();
    287     }
    288 
    289     /**
    290      * Set the callback that indicates the time has been adjusted by the user.
    291      *
    292      * @param onTimeChangedListener the callback, should not be null.
    293      */
    294     public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
    295         mDelegate.setOnTimeChangedListener(onTimeChangedListener);
    296     }
    297 
    298     @Override
    299     public void setEnabled(boolean enabled) {
    300         super.setEnabled(enabled);
    301         mDelegate.setEnabled(enabled);
    302     }
    303 
    304     @Override
    305     public boolean isEnabled() {
    306         return mDelegate.isEnabled();
    307     }
    308 
    309     @Override
    310     public int getBaseline() {
    311         return mDelegate.getBaseline();
    312     }
    313 
    314     /**
    315      * Validates whether current input by the user is a valid time based on the locale. TimePicker
    316      * will show an error message to the user if the time is not valid.
    317      *
    318      * @return {@code true} if the input is valid, {@code false} otherwise
    319      */
    320     public boolean validateInput() {
    321         return mDelegate.validateInput();
    322     }
    323 
    324     @Override
    325     protected Parcelable onSaveInstanceState() {
    326         Parcelable superState = super.onSaveInstanceState();
    327         return mDelegate.onSaveInstanceState(superState);
    328     }
    329 
    330     @Override
    331     protected void onRestoreInstanceState(Parcelable state) {
    332         BaseSavedState ss = (BaseSavedState) state;
    333         super.onRestoreInstanceState(ss.getSuperState());
    334         mDelegate.onRestoreInstanceState(ss);
    335     }
    336 
    337     @Override
    338     public CharSequence getAccessibilityClassName() {
    339         return TimePicker.class.getName();
    340     }
    341 
    342     /** @hide */
    343     @Override
    344     public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
    345         return mDelegate.dispatchPopulateAccessibilityEvent(event);
    346     }
    347 
    348     /** @hide */
    349     @TestApi
    350     public View getHourView() {
    351         return mDelegate.getHourView();
    352     }
    353 
    354     /** @hide */
    355     @TestApi
    356     public View getMinuteView() {
    357         return mDelegate.getMinuteView();
    358     }
    359 
    360     /** @hide */
    361     @TestApi
    362     public View getAmView() {
    363         return mDelegate.getAmView();
    364     }
    365 
    366     /** @hide */
    367     @TestApi
    368     public View getPmView() {
    369         return mDelegate.getPmView();
    370     }
    371 
    372     /**
    373      * A delegate interface that defined the public API of the TimePicker. Allows different
    374      * TimePicker implementations. This would need to be implemented by the TimePicker delegates
    375      * for the real behavior.
    376      */
    377     interface TimePickerDelegate {
    378         void setHour(@IntRange(from = 0, to = 23) int hour);
    379         int getHour();
    380 
    381         void setMinute(@IntRange(from = 0, to = 59) int minute);
    382         int getMinute();
    383 
    384         void setDate(@IntRange(from = 0, to = 23) int hour,
    385                 @IntRange(from = 0, to = 59) int minute);
    386 
    387         void autofill(AutofillValue value);
    388         AutofillValue getAutofillValue();
    389 
    390         void setIs24Hour(boolean is24Hour);
    391         boolean is24Hour();
    392 
    393         boolean validateInput();
    394 
    395         void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener);
    396         void setAutoFillChangeListener(OnTimeChangedListener autoFillChangeListener);
    397 
    398         void setEnabled(boolean enabled);
    399         boolean isEnabled();
    400 
    401         int getBaseline();
    402 
    403         Parcelable onSaveInstanceState(Parcelable superState);
    404         void onRestoreInstanceState(Parcelable state);
    405 
    406         boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
    407         void onPopulateAccessibilityEvent(AccessibilityEvent event);
    408 
    409         /** @hide */
    410         @TestApi View getHourView();
    411 
    412         /** @hide */
    413         @TestApi View getMinuteView();
    414 
    415         /** @hide */
    416         @TestApi View getAmView();
    417 
    418         /** @hide */
    419         @TestApi View getPmView();
    420     }
    421 
    422     static String[] getAmPmStrings(Context context) {
    423         final Locale locale = context.getResources().getConfiguration().locale;
    424         final LocaleData d = LocaleData.get(locale);
    425 
    426         final String[] result = new String[2];
    427         result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
    428         result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
    429         return result;
    430     }
    431 
    432     /**
    433      * An abstract class which can be used as a start for TimePicker implementations
    434      */
    435     abstract static class AbstractTimePickerDelegate implements TimePickerDelegate {
    436         protected final TimePicker mDelegator;
    437         protected final Context mContext;
    438         protected final Locale mLocale;
    439 
    440         protected OnTimeChangedListener mOnTimeChangedListener;
    441         protected OnTimeChangedListener mAutoFillChangeListener;
    442 
    443         // The value that was passed to autofill() - it must be stored because it getAutofillValue()
    444         // must return the exact same value that was autofilled, otherwise the widget will not be
    445         // properly highlighted after autofill().
    446         private long mAutofilledValue;
    447 
    448         public AbstractTimePickerDelegate(@NonNull TimePicker delegator, @NonNull Context context) {
    449             mDelegator = delegator;
    450             mContext = context;
    451             mLocale = context.getResources().getConfiguration().locale;
    452         }
    453 
    454         @Override
    455         public void setOnTimeChangedListener(OnTimeChangedListener callback) {
    456             mOnTimeChangedListener = callback;
    457         }
    458 
    459         @Override
    460         public void setAutoFillChangeListener(OnTimeChangedListener callback) {
    461             mAutoFillChangeListener = callback;
    462         }
    463 
    464         @Override
    465         public final void autofill(AutofillValue value) {
    466             if (value == null || !value.isDate()) {
    467                 Log.w(LOG_TAG, value + " could not be autofilled into " + this);
    468                 return;
    469             }
    470 
    471             final long time = value.getDateValue();
    472 
    473             final Calendar cal = Calendar.getInstance(mLocale);
    474             cal.setTimeInMillis(time);
    475             setDate(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
    476 
    477             // Must set mAutofilledValue *after* calling subclass method to make sure the value
    478             // returned by getAutofillValue() matches it.
    479             mAutofilledValue = time;
    480         }
    481 
    482         @Override
    483         public final AutofillValue getAutofillValue() {
    484             if (mAutofilledValue != 0) {
    485                 return AutofillValue.forDate(mAutofilledValue);
    486             }
    487 
    488             final Calendar cal = Calendar.getInstance(mLocale);
    489             cal.set(Calendar.HOUR_OF_DAY, getHour());
    490             cal.set(Calendar.MINUTE, getMinute());
    491             return AutofillValue.forDate(cal.getTimeInMillis());
    492         }
    493 
    494         /**
    495          * This method must be called every time the value of the hour and/or minute is changed by
    496          * a subclass method.
    497          */
    498         protected void resetAutofilledValue() {
    499             mAutofilledValue = 0;
    500         }
    501 
    502         protected static class SavedState extends View.BaseSavedState {
    503             private final int mHour;
    504             private final int mMinute;
    505             private final boolean mIs24HourMode;
    506             private final int mCurrentItemShowing;
    507 
    508             public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode) {
    509                 this(superState, hour, minute, is24HourMode, 0);
    510             }
    511 
    512             public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
    513                     int currentItemShowing) {
    514                 super(superState);
    515                 mHour = hour;
    516                 mMinute = minute;
    517                 mIs24HourMode = is24HourMode;
    518                 mCurrentItemShowing = currentItemShowing;
    519             }
    520 
    521             private SavedState(Parcel in) {
    522                 super(in);
    523                 mHour = in.readInt();
    524                 mMinute = in.readInt();
    525                 mIs24HourMode = (in.readInt() == 1);
    526                 mCurrentItemShowing = in.readInt();
    527             }
    528 
    529             public int getHour() {
    530                 return mHour;
    531             }
    532 
    533             public int getMinute() {
    534                 return mMinute;
    535             }
    536 
    537             public boolean is24HourMode() {
    538                 return mIs24HourMode;
    539             }
    540 
    541             public int getCurrentItemShowing() {
    542                 return mCurrentItemShowing;
    543             }
    544 
    545             @Override
    546             public void writeToParcel(Parcel dest, int flags) {
    547                 super.writeToParcel(dest, flags);
    548                 dest.writeInt(mHour);
    549                 dest.writeInt(mMinute);
    550                 dest.writeInt(mIs24HourMode ? 1 : 0);
    551                 dest.writeInt(mCurrentItemShowing);
    552             }
    553 
    554             @SuppressWarnings({"unused", "hiding"})
    555             public static final @android.annotation.NonNull Creator<SavedState> CREATOR = new Creator<SavedState>() {
    556                 public SavedState createFromParcel(Parcel in) {
    557                     return new SavedState(in);
    558                 }
    559 
    560                 public SavedState[] newArray(int size) {
    561                     return new SavedState[size];
    562                 }
    563             };
    564         }
    565     }
    566 
    567     @Override
    568     public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
    569         // This view is self-sufficient for autofill, so it needs to call
    570         // onProvideAutoFillStructure() to fill itself, but it does not need to call
    571         // dispatchProvideAutoFillStructure() to fill its children.
    572         structure.setAutofillId(getAutofillId());
    573         onProvideAutofillStructure(structure, flags);
    574     }
    575 
    576     @Override
    577     public void autofill(AutofillValue value) {
    578         if (!isEnabled()) return;
    579 
    580         mDelegate.autofill(value);
    581     }
    582 
    583     @Override
    584     public @AutofillType int getAutofillType() {
    585         return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE;
    586     }
    587 
    588     @Override
    589     public AutofillValue getAutofillValue() {
    590         return isEnabled() ? mDelegate.getAutofillValue() : null;
    591     }
    592 }
    593