blob: 23f258284efa2653f15746c114dc10c74d4e48dd [file] [log] [blame]
Aurimas Liutikasab14d822017-01-24 17:46:10 -08001/*
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
17package android.widget;
18
19import android.content.Context;
Roozbeh Pournaderf3cda892017-07-13 15:19:45 -070020import android.os.LocaleList;
Aurimas Liutikasab14d822017-01-24 17:46:10 -080021import android.text.Editable;
22import android.text.InputFilter;
David Ogutu07b2bb72018-03-15 11:36:30 -040023import android.text.TextUtils;
Aurimas Liutikasab14d822017-01-24 17:46:10 -080024import android.text.TextWatcher;
25import android.util.AttributeSet;
26import android.util.MathUtils;
27import android.view.View;
David Ogutub8752382018-03-15 11:36:30 -040028import android.view.accessibility.AccessibilityManager;
Aurimas Liutikasab14d822017-01-24 17:46:10 -080029
30import 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 */
38public 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;
David Ogutu07b2bb72018-03-15 11:36:30 -040059 private boolean mTimeSet;
Aurimas Liutikasab14d822017-01-24 17:46:10 -080060
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
Alan Viverette51efddb2017-04-05 10:00:01 -040083 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);
Aurimas Liutikasab14d822017-01-24 17:46:10 -080089
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) {
David Ogutub8752382018-03-15 11:36:30 -040099 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 }
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800106 }
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
Alan Viverette51efddb2017-04-05 10:00:01 -0400122 mAmPmSpinner = findViewById(R.id.am_pm_spinner);
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800123 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)});
Roozbeh Pournaderf3cda892017-07-13 15:19:45 -0700154 final LocaleList locales = mContext.getResources().getConfiguration().getLocales();
155 mHourEditText.setImeHintLocales(locales);
156 mMinuteEditText.setImeHintLocales(locales);
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800157 }
158
159 boolean validateInput() {
David Ogutu07b2bb72018-03-15 11:36:30 -0400160 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);
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800169 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
David Ogutu07b2bb72018-03-15 11:36:30 -0400185 private void setTimeSet(boolean timeSet) {
186 mTimeSet = mTimeSet || timeSet;
187 }
188
189 private boolean isTimeSet() {
190 return mTimeSet;
191 }
192
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800193 /**
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) {
David Ogutu93ac6d02018-02-05 17:08:11 -0500201 final String hourFormat = "%d";
202 final String minuteFormat = "%02d";
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800203
204 mIs24Hour = is24Hour;
205 mHourFormatStartsAtZero = hourFormatStartsAtZero;
206
207 mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE);
208
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800209 if (amOrPm == AM) {
210 mAmPmSpinner.setSelection(0);
211 } else {
212 mAmPmSpinner.setSelection(1);
213 }
214
David Ogutu07b2bb72018-03-15 11:36:30 -0400215 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
Aurimas Liutikasc51c5f22017-02-22 16:56:43 -0800223
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800224 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));
David Ogutu07b2bb72018-03-15 11:36:30 -0400240 setTimeSet(true);
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800241 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);
David Ogutu07b2bb72018-03-15 11:36:30 -0400256 setTimeSet(true);
Aurimas Liutikasab14d822017-01-24 17:46:10 -0800257 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}