TextInputTimePickerView.java revision f3cda89def7386db5d74fa76a36a911710e02f1c
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
17package android.widget;
18
19import android.content.Context;
20import android.os.LocaleList;
21import android.text.Editable;
22import android.text.InputFilter;
23import android.text.TextWatcher;
24import android.util.AttributeSet;
25import android.util.MathUtils;
26import android.view.View;
27
28import 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 */
36public 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 format = "%d";
178
179        mIs24Hour = is24Hour;
180        mHourFormatStartsAtZero = hourFormatStartsAtZero;
181
182        mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE);
183
184        if (amOrPm == AM) {
185            mAmPmSpinner.setSelection(0);
186        } else {
187            mAmPmSpinner.setSelection(1);
188        }
189
190        mHourEditText.setText(String.format(format, localizedHour));
191        mMinuteEditText.setText(String.format(format, minute));
192
193        if (mErrorShowing) {
194            validateInput();
195        }
196    }
197
198    private boolean parseAndSetHourInternal(String input) {
199        try {
200            final int hour = Integer.parseInt(input);
201            if (!isValidLocalizedHour(hour)) {
202                final int minHour = mHourFormatStartsAtZero ? 0 : 1;
203                final int maxHour = mIs24Hour ? 23 : 11 + minHour;
204                mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(
205                        MathUtils.constrain(hour, minHour, maxHour)));
206                return false;
207            }
208            mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour));
209            return true;
210        } catch (NumberFormatException e) {
211            // Do nothing since we cannot parse the input.
212            return false;
213        }
214    }
215
216    private boolean parseAndSetMinuteInternal(String input) {
217        try {
218            final int minutes = Integer.parseInt(input);
219            if (minutes < 0 || minutes > 59) {
220                mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59));
221                return false;
222            }
223            mListener.onValueChanged(MINUTES, minutes);
224            return true;
225        } catch (NumberFormatException e) {
226            // Do nothing since we cannot parse the input.
227            return false;
228        }
229    }
230
231    private boolean isValidLocalizedHour(int localizedHour) {
232        final int minHour = mHourFormatStartsAtZero ? 0 : 1;
233        final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
234        return localizedHour >= minHour && localizedHour <= maxHour;
235    }
236
237    private int getHourOfDayFromLocalizedHour(int localizedHour) {
238        int hourOfDay = localizedHour;
239        if (mIs24Hour) {
240            if (!mHourFormatStartsAtZero && localizedHour == 24) {
241                hourOfDay = 0;
242            }
243        } else {
244            if (!mHourFormatStartsAtZero && localizedHour == 12) {
245                hourOfDay = 0;
246            }
247            if (mAmPmSpinner.getSelectedItemPosition() == 1) {
248                hourOfDay += 12;
249            }
250        }
251        return hourOfDay;
252    }
253}
254