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