Home | History | Annotate | Download | only in widget
      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 android.widget;
     18 
     19 import android.content.Context;
     20 import android.os.LocaleList;
     21 import android.text.Editable;
     22 import android.text.InputFilter;
     23 import android.text.TextUtils;
     24 import android.text.TextWatcher;
     25 import android.util.AttributeSet;
     26 import android.util.MathUtils;
     27 import android.view.View;
     28 import android.view.accessibility.AccessibilityManager;
     29 
     30 import com.android.internal.R;
     31 
     32 /**
     33  * View to show text input based time picker with hour and minute fields and an optional AM/PM
     34  * spinner.
     35  *
     36  * @hide
     37  */
     38 public class TextInputTimePickerView extends RelativeLayout {
     39     public static final int HOURS = 0;
     40     public static final int MINUTES = 1;
     41     public static final int AMPM = 2;
     42 
     43     private static final int AM = 0;
     44     private static final int PM = 1;
     45 
     46     private final EditText mHourEditText;
     47     private final EditText mMinuteEditText;
     48     private final TextView mInputSeparatorView;
     49     private final Spinner mAmPmSpinner;
     50     private final TextView mErrorLabel;
     51     private final TextView mHourLabel;
     52     private final TextView mMinuteLabel;
     53 
     54     private boolean mIs24Hour;
     55     private boolean mHourFormatStartsAtZero;
     56     private OnValueTypedListener mListener;
     57 
     58     private boolean mErrorShowing;
     59     private boolean mTimeSet;
     60 
     61     interface OnValueTypedListener {
     62         void onValueChanged(int inputType, int newValue);
     63     }
     64 
     65     public TextInputTimePickerView(Context context) {
     66         this(context, null);
     67     }
     68 
     69     public TextInputTimePickerView(Context context, AttributeSet attrs) {
     70         this(context, attrs, 0);
     71     }
     72 
     73     public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) {
     74         this(context, attrs, defStyle, 0);
     75     }
     76 
     77     public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle,
     78             int defStyleRes) {
     79         super(context, attrs, defStyle, defStyleRes);
     80 
     81         inflate(context, R.layout.time_picker_text_input_material, this);
     82 
     83         mHourEditText = findViewById(R.id.input_hour);
     84         mMinuteEditText = findViewById(R.id.input_minute);
     85         mInputSeparatorView = findViewById(R.id.input_separator);
     86         mErrorLabel = findViewById(R.id.label_error);
     87         mHourLabel = findViewById(R.id.label_hour);
     88         mMinuteLabel = findViewById(R.id.label_minute);
     89 
     90         mHourEditText.addTextChangedListener(new TextWatcher() {
     91             @Override
     92             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
     93 
     94             @Override
     95             public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
     96 
     97             @Override
     98             public void afterTextChanged(Editable editable) {
     99                 if (parseAndSetHourInternal(editable.toString()) && editable.length() > 1) {
    100                     AccessibilityManager am = (AccessibilityManager) context.getSystemService(
    101                             context.ACCESSIBILITY_SERVICE);
    102                     if (!am.isEnabled()) {
    103                         mMinuteEditText.requestFocus();
    104                     }
    105                 }
    106             }
    107         });
    108 
    109         mMinuteEditText.addTextChangedListener(new TextWatcher() {
    110             @Override
    111             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
    112 
    113             @Override
    114             public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
    115 
    116             @Override
    117             public void afterTextChanged(Editable editable) {
    118                 parseAndSetMinuteInternal(editable.toString());
    119             }
    120         });
    121 
    122         mAmPmSpinner = findViewById(R.id.am_pm_spinner);
    123         final String[] amPmStrings = TimePicker.getAmPmStrings(context);
    124         ArrayAdapter<CharSequence> adapter =
    125                 new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_dropdown_item);
    126         adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[0]));
    127         adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[1]));
    128         mAmPmSpinner.setAdapter(adapter);
    129         mAmPmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    130             @Override
    131             public void onItemSelected(AdapterView<?> adapterView, View view, int position,
    132                     long id) {
    133                 if (position == 0) {
    134                     mListener.onValueChanged(AMPM, AM);
    135                 } else {
    136                     mListener.onValueChanged(AMPM, PM);
    137                 }
    138             }
    139 
    140             @Override
    141             public void onNothingSelected(AdapterView<?> adapterView) {}
    142         });
    143     }
    144 
    145     void setListener(OnValueTypedListener listener) {
    146         mListener = listener;
    147     }
    148 
    149     void setHourFormat(int maxCharLength) {
    150         mHourEditText.setFilters(new InputFilter[] {
    151                 new InputFilter.LengthFilter(maxCharLength)});
    152         mMinuteEditText.setFilters(new InputFilter[] {
    153                 new InputFilter.LengthFilter(maxCharLength)});
    154         final LocaleList locales = mContext.getResources().getConfiguration().getLocales();
    155         mHourEditText.setImeHintLocales(locales);
    156         mMinuteEditText.setImeHintLocales(locales);
    157     }
    158 
    159     boolean validateInput() {
    160         final String hourText = TextUtils.isEmpty(mHourEditText.getText())
    161                 ? mHourEditText.getHint().toString()
    162                 : mHourEditText.getText().toString();
    163         final String minuteText = TextUtils.isEmpty(mMinuteEditText.getText())
    164                 ? mMinuteEditText.getHint().toString()
    165                 : mMinuteEditText.getText().toString();
    166 
    167         final boolean inputValid = parseAndSetHourInternal(hourText)
    168                 && parseAndSetMinuteInternal(minuteText);
    169         setError(!inputValid);
    170         return inputValid;
    171     }
    172 
    173     void updateSeparator(String separatorText) {
    174         mInputSeparatorView.setText(separatorText);
    175     }
    176 
    177     private void setError(boolean enabled) {
    178         mErrorShowing = enabled;
    179 
    180         mErrorLabel.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
    181         mHourLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
    182         mMinuteLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
    183     }
    184 
    185     private void setTimeSet(boolean timeSet) {
    186         mTimeSet = mTimeSet || timeSet;
    187     }
    188 
    189     private boolean isTimeSet() {
    190         return mTimeSet;
    191     }
    192 
    193     /**
    194      * Computes the display value and updates the text of the view.
    195      * <p>
    196      * This method should be called whenever the current value or display
    197      * properties (leading zeroes, max digits) change.
    198      */
    199     void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour,
    200             boolean hourFormatStartsAtZero) {
    201         final String hourFormat = "%d";
    202         final String minuteFormat = "%02d";
    203 
    204         mIs24Hour = is24Hour;
    205         mHourFormatStartsAtZero = hourFormatStartsAtZero;
    206 
    207         mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE);
    208 
    209         if (amOrPm == AM) {
    210             mAmPmSpinner.setSelection(0);
    211         } else {
    212             mAmPmSpinner.setSelection(1);
    213         }
    214 
    215         if (isTimeSet()) {
    216             mHourEditText.setText(String.format(hourFormat, localizedHour));
    217             mMinuteEditText.setText(String.format(minuteFormat, minute));
    218         } else {
    219             mHourEditText.setHint(String.format(hourFormat, localizedHour));
    220             mMinuteEditText.setHint(String.format(minuteFormat, minute));
    221         }
    222 
    223 
    224         if (mErrorShowing) {
    225             validateInput();
    226         }
    227     }
    228 
    229     private boolean parseAndSetHourInternal(String input) {
    230         try {
    231             final int hour = Integer.parseInt(input);
    232             if (!isValidLocalizedHour(hour)) {
    233                 final int minHour = mHourFormatStartsAtZero ? 0 : 1;
    234                 final int maxHour = mIs24Hour ? 23 : 11 + minHour;
    235                 mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(
    236                         MathUtils.constrain(hour, minHour, maxHour)));
    237                 return false;
    238             }
    239             mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour));
    240             setTimeSet(true);
    241             return true;
    242         } catch (NumberFormatException e) {
    243             // Do nothing since we cannot parse the input.
    244             return false;
    245         }
    246     }
    247 
    248     private boolean parseAndSetMinuteInternal(String input) {
    249         try {
    250             final int minutes = Integer.parseInt(input);
    251             if (minutes < 0 || minutes > 59) {
    252                 mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59));
    253                 return false;
    254             }
    255             mListener.onValueChanged(MINUTES, minutes);
    256             setTimeSet(true);
    257             return true;
    258         } catch (NumberFormatException e) {
    259             // Do nothing since we cannot parse the input.
    260             return false;
    261         }
    262     }
    263 
    264     private boolean isValidLocalizedHour(int localizedHour) {
    265         final int minHour = mHourFormatStartsAtZero ? 0 : 1;
    266         final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
    267         return localizedHour >= minHour && localizedHour <= maxHour;
    268     }
    269 
    270     private int getHourOfDayFromLocalizedHour(int localizedHour) {
    271         int hourOfDay = localizedHour;
    272         if (mIs24Hour) {
    273             if (!mHourFormatStartsAtZero && localizedHour == 24) {
    274                 hourOfDay = 0;
    275             }
    276         } else {
    277             if (!mHourFormatStartsAtZero && localizedHour == 12) {
    278                 hourOfDay = 0;
    279             }
    280             if (mAmPmSpinner.getSelectedItemPosition() == 1) {
    281                 hourOfDay += 12;
    282             }
    283         }
    284         return hourOfDay;
    285     }
    286 }
    287