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 { onValueChanged(int inputType, int newValue)62 void onValueChanged(int inputType, int newValue); 63 } 64 TextInputTimePickerView(Context context)65 public TextInputTimePickerView(Context context) { 66 this(context, null); 67 } 68 TextInputTimePickerView(Context context, AttributeSet attrs)69 public TextInputTimePickerView(Context context, AttributeSet attrs) { 70 this(context, attrs, 0); 71 } 72 TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle)73 public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) { 74 this(context, attrs, defStyle, 0); 75 } 76 TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle, int defStyleRes)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 setListener(OnValueTypedListener listener)145 void setListener(OnValueTypedListener listener) { 146 mListener = listener; 147 } 148 setHourFormat(int maxCharLength)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 validateInput()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 updateSeparator(String separatorText)173 void updateSeparator(String separatorText) { 174 mInputSeparatorView.setText(separatorText); 175 } 176 setError(boolean enabled)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 setTimeSet(boolean timeSet)185 private void setTimeSet(boolean timeSet) { 186 mTimeSet = mTimeSet || timeSet; 187 } 188 isTimeSet()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 */ updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour, boolean hourFormatStartsAtZero)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 parseAndSetHourInternal(String input)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 parseAndSetMinuteInternal(String input)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 isValidLocalizedHour(int localizedHour)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 getHourOfDayFromLocalizedHour(int localizedHour)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