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