Home | History | Annotate | Download | only in picker
      1 /*
      2  * Copyright (C) 2017 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 androidx.leanback.widget.picker;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.text.TextUtils;
     22 import android.text.format.DateFormat;
     23 import android.util.AttributeSet;
     24 import android.view.View;
     25 
     26 import androidx.annotation.IntRange;
     27 import androidx.leanback.R;
     28 
     29 import java.text.SimpleDateFormat;
     30 import java.util.ArrayList;
     31 import java.util.Calendar;
     32 import java.util.List;
     33 import java.util.Locale;
     34 
     35 /**
     36  * {@link TimePicker} is a direct subclass of {@link Picker}.
     37  * <p>
     38  * This class is a widget for selecting time and displays it according to the formatting for the
     39  * current system locale. The time can be selected by hour, minute, and AM/PM picker columns.
     40  * The AM/PM mode is determined by either explicitly setting the current mode through
     41  * {@link #setIs24Hour(boolean)} or the widget attribute {@code is24HourFormat} (true for 24-hour
     42  * mode, false for 12-hour mode). Otherwise, TimePicker retrieves the mode based on the current
     43  * context. In 24-hour mode, TimePicker displays only the hour and minute columns.
     44  * <p>
     45  * This widget can show the current time as the initial value if {@code useCurrentTime} is set to
     46  * true. Each individual time picker field can be set at any time by calling {@link #setHour(int)},
     47  * {@link #setMinute(int)} using 24-hour time format. The time format can also be changed at any
     48  * time by calling {@link #setIs24Hour(boolean)}, and the AM/PM picker column will be activated or
     49  * deactivated accordingly.
     50  *
     51  * @attr ref R.styleable#lbTimePicker_is24HourFormat
     52  * @attr ref R.styleable#lbTimePicker_useCurrentTime
     53  */
     54 public class TimePicker extends Picker {
     55 
     56     static final String TAG = "TimePicker";
     57 
     58     private static final int AM_INDEX = 0;
     59     private static final int PM_INDEX = 1;
     60 
     61     private static final int HOURS_IN_HALF_DAY = 12;
     62     PickerColumn mHourColumn;
     63     PickerColumn mMinuteColumn;
     64     PickerColumn mAmPmColumn;
     65     int mColHourIndex;
     66     int mColMinuteIndex;
     67     int mColAmPmIndex;
     68 
     69     private final PickerUtility.TimeConstant mConstant;
     70 
     71     private boolean mIs24hFormat;
     72 
     73     private int mCurrentHour;
     74     private int mCurrentMinute;
     75     private int mCurrentAmPmIndex;
     76 
     77     private String mTimePickerFormat;
     78 
     79     /**
     80      * Constructor called when inflating a TimePicker widget. This version uses a default style of
     81      * 0, so the only attribute values applied are those in the Context's Theme and the given
     82      * AttributeSet.
     83      *
     84      * @param context the context this TimePicker widget is associated with through which we can
     85      *                access the current theme attributes and resources
     86      * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
     87      */
     88     public TimePicker(Context context, AttributeSet attrs) {
     89         this(context, attrs, 0);
     90     }
     91 
     92     /**
     93      * Constructor called when inflating a TimePicker widget.
     94      *
     95      * @param context the context this TimePicker widget is associated with through which we can
     96      *                access the current theme attributes and resources
     97      * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
     98      * @param defStyleAttr An attribute in the current theme that contains a reference to a style
     99      *                     resource that supplies default values for the widget. Can be 0 to not
    100      *                     look for defaults.
    101      */
    102     public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
    103         super(context, attrs, defStyleAttr);
    104 
    105         mConstant = PickerUtility.getTimeConstantInstance(Locale.getDefault(),
    106                 context.getResources());
    107 
    108         final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
    109                 R.styleable.lbTimePicker);
    110         mIs24hFormat = attributesArray.getBoolean(R.styleable.lbTimePicker_is24HourFormat,
    111                 DateFormat.is24HourFormat(context));
    112         boolean useCurrentTime = attributesArray.getBoolean(R.styleable.lbTimePicker_useCurrentTime,
    113                 true);
    114 
    115         // The following 2 methods must be called after setting mIs24hFormat since this attribute is
    116         // used to extract the time format string.
    117         updateColumns();
    118         updateColumnsRange();
    119 
    120         if (useCurrentTime) {
    121             Calendar currentDate = PickerUtility.getCalendarForLocale(null,
    122                     mConstant.locale);
    123             setHour(currentDate.get(Calendar.HOUR_OF_DAY));
    124             setMinute(currentDate.get(Calendar.MINUTE));
    125             setAmPmValue();
    126         }
    127     }
    128 
    129     private static boolean updateMin(PickerColumn column, int value) {
    130         if (value != column.getMinValue()) {
    131             column.setMinValue(value);
    132             return true;
    133         }
    134         return false;
    135     }
    136 
    137     private static boolean updateMax(PickerColumn column, int value) {
    138         if (value != column.getMaxValue()) {
    139             column.setMaxValue(value);
    140             return true;
    141         }
    142         return false;
    143     }
    144 
    145     /**
    146      * @return The best localized representation of time for the current locale
    147      */
    148     String getBestHourMinutePattern() {
    149         final String hourPattern;
    150         if (PickerUtility.SUPPORTS_BEST_DATE_TIME_PATTERN) {
    151             hourPattern = DateFormat.getBestDateTimePattern(mConstant.locale, mIs24hFormat ? "Hma"
    152                     : "hma");
    153         } else {
    154             // Using short style to avoid picking extra fields e.g. time zone in the returned time
    155             // format.
    156             final java.text.DateFormat dateFormat =
    157                     SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT, mConstant.locale);
    158             if (dateFormat instanceof SimpleDateFormat) {
    159                 String defaultPattern = ((SimpleDateFormat) dateFormat).toPattern();
    160                 defaultPattern = defaultPattern.replace("s", "");
    161                 if (mIs24hFormat) {
    162                     defaultPattern = defaultPattern.replace('h', 'H').replace("a", "");
    163                 }
    164                 hourPattern = defaultPattern;
    165             } else {
    166                 hourPattern = mIs24hFormat ? "H:mma" : "h:mma";
    167             }
    168         }
    169         return TextUtils.isEmpty(hourPattern) ? "h:mma" : hourPattern;
    170     }
    171 
    172     /**
    173      * Extracts the separators used to separate time fields (including before the first and after
    174      * the last time field). The separators can vary based on the individual locale and 12 or
    175      * 24 hour time format, defined in the Unicode CLDR and cannot be supposed to be ":".
    176      *
    177      * See http://unicode.org/cldr/trac/browser/trunk/common/main
    178      *
    179      * For example, for english in 12 hour format
    180      * (time pattern of "h:mm a"), this will return {"", ":", "", ""}, where the first separator
    181      * indicates nothing needs to be displayed to the left of the hour field, ":" needs to be
    182      * displayed to the right of hour field, and so forth.
    183      *
    184      * @return The ArrayList of separators to populate between the actual time fields in the
    185      * TimePicker.
    186      */
    187     List<CharSequence> extractSeparators() {
    188         // Obtain the time format string per the current locale (e.g. h:mm a)
    189         String hmaPattern = getBestHourMinutePattern();
    190 
    191         List<CharSequence> separators = new ArrayList<>();
    192         StringBuilder sb = new StringBuilder();
    193         char lastChar = '\0';
    194         // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats
    195         final char[] timeFormats = {'H', 'h', 'K', 'k', 'm', 'M', 'a'};
    196         boolean processingQuote = false;
    197         for (int i = 0; i < hmaPattern.length(); i++) {
    198             char c = hmaPattern.charAt(i);
    199             if (c == ' ') {
    200                 continue;
    201             }
    202             if (c == '\'') {
    203                 if (!processingQuote) {
    204                     sb.setLength(0);
    205                     processingQuote = true;
    206                 } else {
    207                     processingQuote = false;
    208                 }
    209                 continue;
    210             }
    211             if (processingQuote) {
    212                 sb.append(c);
    213             } else {
    214                 if (isAnyOf(c, timeFormats)) {
    215                     if (c != lastChar) {
    216                         separators.add(sb.toString());
    217                         sb.setLength(0);
    218                     }
    219                 } else {
    220                     sb.append(c);
    221                 }
    222             }
    223             lastChar = c;
    224         }
    225         separators.add(sb.toString());
    226         return separators;
    227     }
    228 
    229     private static boolean isAnyOf(char c, char[] any) {
    230         for (int i = 0; i < any.length; i++) {
    231             if (c == any[i]) {
    232                 return true;
    233             }
    234         }
    235         return false;
    236     }
    237 
    238     /**
    239      *
    240      * @return the time picker format string based on the current system locale and the layout
    241      *         direction
    242      */
    243     private String extractTimeFields() {
    244         // Obtain the time format string per the current locale (e.g. h:mm a)
    245         String hmaPattern = getBestHourMinutePattern();
    246 
    247         boolean isRTL = TextUtils.getLayoutDirectionFromLocale(mConstant.locale) == View
    248                 .LAYOUT_DIRECTION_RTL;
    249         boolean isAmPmAtEnd = (hmaPattern.indexOf('a') >= 0)
    250                 ? (hmaPattern.indexOf("a") > hmaPattern.indexOf("m")) : true;
    251         // Hour will always appear to the left of minutes regardless of layout direction.
    252         String timePickerFormat = isRTL ? "mh" : "hm";
    253 
    254         if (is24Hour()) {
    255             return timePickerFormat;
    256         } else {
    257             return isAmPmAtEnd ? (timePickerFormat + "a") : ("a" + timePickerFormat);
    258         }
    259     }
    260 
    261     private void updateColumns() {
    262         String timePickerFormat = getBestHourMinutePattern();
    263         if (TextUtils.equals(timePickerFormat, mTimePickerFormat)) {
    264             return;
    265         }
    266         mTimePickerFormat = timePickerFormat;
    267 
    268         String timeFieldsPattern = extractTimeFields();
    269         List<CharSequence> separators = extractSeparators();
    270         if (separators.size() != (timeFieldsPattern.length() + 1)) {
    271             throw new IllegalStateException("Separators size: " + separators.size() + " must equal"
    272                     + " the size of timeFieldsPattern: " + timeFieldsPattern.length() + " + 1");
    273         }
    274         setSeparators(separators);
    275         timeFieldsPattern = timeFieldsPattern.toUpperCase();
    276 
    277         mHourColumn = mMinuteColumn = mAmPmColumn = null;
    278         mColHourIndex = mColMinuteIndex = mColAmPmIndex = -1;
    279 
    280         ArrayList<PickerColumn> columns = new ArrayList<>(3);
    281         for (int i = 0; i < timeFieldsPattern.length(); i++) {
    282             switch (timeFieldsPattern.charAt(i)) {
    283                 case 'H':
    284                     columns.add(mHourColumn = new PickerColumn());
    285                     mHourColumn.setStaticLabels(mConstant.hours24);
    286                     mColHourIndex = i;
    287                     break;
    288                 case 'M':
    289                     columns.add(mMinuteColumn = new PickerColumn());
    290                     mMinuteColumn.setStaticLabels(mConstant.minutes);
    291                     mColMinuteIndex = i;
    292                     break;
    293                 case 'A':
    294                     columns.add(mAmPmColumn = new PickerColumn());
    295                     mAmPmColumn.setStaticLabels(mConstant.ampm);
    296                     mColAmPmIndex = i;
    297                     updateMin(mAmPmColumn, 0);
    298                     updateMax(mAmPmColumn, 1);
    299                     break;
    300                 default:
    301                     throw new IllegalArgumentException("Invalid time picker format.");
    302             }
    303         }
    304         setColumns(columns);
    305     }
    306 
    307     private void updateColumnsRange() {
    308         // updateHourColumn(false);
    309         updateMin(mHourColumn, mIs24hFormat ? 0 : 1);
    310         updateMax(mHourColumn, mIs24hFormat ? 23 : 12);
    311 
    312         updateMin(mMinuteColumn, 0);
    313         updateMax(mMinuteColumn, 59);
    314 
    315         if (mAmPmColumn != null) {
    316             updateMin(mAmPmColumn, 0);
    317             updateMax(mAmPmColumn, 1);
    318         }
    319     }
    320 
    321     /**
    322      * Updates the value of AM/PM column for a 12 hour time format. The correct value should already
    323      * be calculated before this method is called by calling setHour.
    324      */
    325     private void setAmPmValue() {
    326         if (!is24Hour()) {
    327             setColumnValue(mColAmPmIndex, mCurrentAmPmIndex, false);
    328         }
    329     }
    330 
    331     /**
    332      * Sets the currently selected hour using a 24-hour time.
    333      *
    334      * @param hour the hour to set, in the range (0-23)
    335      * @see #getHour()
    336      */
    337     public void setHour(@IntRange(from = 0, to = 23) int hour) {
    338         if (hour < 0 || hour > 23) {
    339             throw new IllegalArgumentException("hour: " + hour + " is not in [0-23] range in");
    340         }
    341         mCurrentHour = hour;
    342         if (!is24Hour()) {
    343             if (mCurrentHour >= HOURS_IN_HALF_DAY) {
    344                 mCurrentAmPmIndex = PM_INDEX;
    345                 if (mCurrentHour > HOURS_IN_HALF_DAY) {
    346                     mCurrentHour -= HOURS_IN_HALF_DAY;
    347                 }
    348             } else {
    349                 mCurrentAmPmIndex = AM_INDEX;
    350                 if (mCurrentHour == 0) {
    351                     mCurrentHour = HOURS_IN_HALF_DAY;
    352                 }
    353             }
    354             setAmPmValue();
    355         }
    356         setColumnValue(mColHourIndex, mCurrentHour, false);
    357     }
    358 
    359     /**
    360      * Returns the currently selected hour using 24-hour time.
    361      *
    362      * @return the currently selected hour in the range (0-23)
    363      * @see #setHour(int)
    364      */
    365     public int getHour() {
    366         if (mIs24hFormat) {
    367             return mCurrentHour;
    368         }
    369         if (mCurrentAmPmIndex == AM_INDEX) {
    370             return mCurrentHour % HOURS_IN_HALF_DAY;
    371         }
    372         return (mCurrentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
    373     }
    374 
    375     /**
    376      * Sets the currently selected minute.
    377      *
    378      * @param minute the minute to set, in the range (0-59)
    379      * @see #getMinute()
    380      */
    381     public void setMinute(@IntRange(from = 0, to = 59) int minute) {
    382         if (minute < 0 || minute > 59) {
    383             throw new IllegalArgumentException("minute: " + minute + " is not in [0-59] range.");
    384         }
    385         mCurrentMinute = minute;
    386         setColumnValue(mColMinuteIndex, mCurrentMinute, false);
    387     }
    388 
    389     /**
    390      * Returns the currently selected minute.
    391      *
    392      * @return the currently selected minute, in the range (0-59)
    393      * @see #setMinute(int)
    394      */
    395     public int getMinute() {
    396         return mCurrentMinute;
    397     }
    398 
    399     /**
    400      * Sets whether this widget displays a 24-hour mode or a 12-hour mode with an AM/PM picker.
    401      *
    402      * @param is24Hour {@code true} to display in 24-hour mode,
    403      *                 {@code false} ti display in 12-hour mode with AM/PM.
    404      * @see #is24Hour()
    405      */
    406     public void setIs24Hour(boolean is24Hour) {
    407         if (mIs24hFormat == is24Hour) {
    408             return;
    409         }
    410         // the ordering of these statements is important
    411         int currentHour = getHour();
    412         int currentMinute = getMinute();
    413         mIs24hFormat = is24Hour;
    414         updateColumns();
    415         updateColumnsRange();
    416 
    417         setHour(currentHour);
    418         setMinute(currentMinute);
    419         setAmPmValue();
    420     }
    421 
    422     /**
    423      * @return {@code true} if this widget displays time in 24-hour mode,
    424      *         {@code false} otherwise.
    425      *
    426      * @see #setIs24Hour(boolean)
    427      */
    428     public boolean is24Hour() {
    429         return mIs24hFormat;
    430     }
    431 
    432     /**
    433      * Only meaningful for a 12-hour time.
    434      *
    435      * @return {@code true} if the currently selected time is in PM,
    436      *         {@code false} if the currently selected time in in AM.
    437      */
    438     public boolean isPm() {
    439         return (mCurrentAmPmIndex == PM_INDEX);
    440     }
    441 
    442     @Override
    443     public void onColumnValueChanged(int columnIndex, int newValue) {
    444         if (columnIndex == mColHourIndex) {
    445             mCurrentHour = newValue;
    446         } else if (columnIndex == mColMinuteIndex) {
    447             mCurrentMinute = newValue;
    448         } else if (columnIndex == mColAmPmIndex) {
    449             mCurrentAmPmIndex = newValue;
    450         } else {
    451             throw new IllegalArgumentException("Invalid column index.");
    452         }
    453     }
    454 }
    455