| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.widget; |
| |
| import android.content.Context; |
| import android.os.LocaleList; |
| import android.text.Editable; |
| import android.text.InputFilter; |
| import android.text.TextUtils; |
| import android.text.TextWatcher; |
| import android.util.AttributeSet; |
| import android.util.MathUtils; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityManager; |
| |
| import com.android.internal.R; |
| |
| /** |
| * View to show text input based time picker with hour and minute fields and an optional AM/PM |
| * spinner. |
| * |
| * @hide |
| */ |
| public class TextInputTimePickerView extends RelativeLayout { |
| public static final int HOURS = 0; |
| public static final int MINUTES = 1; |
| public static final int AMPM = 2; |
| |
| private static final int AM = 0; |
| private static final int PM = 1; |
| |
| private final EditText mHourEditText; |
| private final EditText mMinuteEditText; |
| private final TextView mInputSeparatorView; |
| private final Spinner mAmPmSpinner; |
| private final TextView mErrorLabel; |
| private final TextView mHourLabel; |
| private final TextView mMinuteLabel; |
| |
| private boolean mIs24Hour; |
| private boolean mHourFormatStartsAtZero; |
| private OnValueTypedListener mListener; |
| |
| private boolean mErrorShowing; |
| private boolean mTimeSet; |
| |
| interface OnValueTypedListener { |
| void onValueChanged(int inputType, int newValue); |
| } |
| |
| public TextInputTimePickerView(Context context) { |
| this(context, null); |
| } |
| |
| public TextInputTimePickerView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) { |
| this(context, attrs, defStyle, 0); |
| } |
| |
| public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle, |
| int defStyleRes) { |
| super(context, attrs, defStyle, defStyleRes); |
| |
| inflate(context, R.layout.time_picker_text_input_material, this); |
| |
| mHourEditText = findViewById(R.id.input_hour); |
| mMinuteEditText = findViewById(R.id.input_minute); |
| mInputSeparatorView = findViewById(R.id.input_separator); |
| mErrorLabel = findViewById(R.id.label_error); |
| mHourLabel = findViewById(R.id.label_hour); |
| mMinuteLabel = findViewById(R.id.label_minute); |
| |
| mHourEditText.addTextChangedListener(new TextWatcher() { |
| @Override |
| public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} |
| |
| @Override |
| public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} |
| |
| @Override |
| public void afterTextChanged(Editable editable) { |
| if (parseAndSetHourInternal(editable.toString()) && editable.length() > 1) { |
| AccessibilityManager am = (AccessibilityManager) context.getSystemService( |
| context.ACCESSIBILITY_SERVICE); |
| if (!am.isEnabled()) { |
| mMinuteEditText.requestFocus(); |
| } |
| } |
| } |
| }); |
| |
| mMinuteEditText.addTextChangedListener(new TextWatcher() { |
| @Override |
| public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} |
| |
| @Override |
| public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} |
| |
| @Override |
| public void afterTextChanged(Editable editable) { |
| parseAndSetMinuteInternal(editable.toString()); |
| } |
| }); |
| |
| mAmPmSpinner = findViewById(R.id.am_pm_spinner); |
| final String[] amPmStrings = TimePicker.getAmPmStrings(context); |
| ArrayAdapter<CharSequence> adapter = |
| new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_dropdown_item); |
| adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[0])); |
| adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[1])); |
| mAmPmSpinner.setAdapter(adapter); |
| mAmPmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { |
| @Override |
| public void onItemSelected(AdapterView<?> adapterView, View view, int position, |
| long id) { |
| if (position == 0) { |
| mListener.onValueChanged(AMPM, AM); |
| } else { |
| mListener.onValueChanged(AMPM, PM); |
| } |
| } |
| |
| @Override |
| public void onNothingSelected(AdapterView<?> adapterView) {} |
| }); |
| } |
| |
| void setListener(OnValueTypedListener listener) { |
| mListener = listener; |
| } |
| |
| void setHourFormat(int maxCharLength) { |
| mHourEditText.setFilters(new InputFilter[] { |
| new InputFilter.LengthFilter(maxCharLength)}); |
| mMinuteEditText.setFilters(new InputFilter[] { |
| new InputFilter.LengthFilter(maxCharLength)}); |
| final LocaleList locales = mContext.getResources().getConfiguration().getLocales(); |
| mHourEditText.setImeHintLocales(locales); |
| mMinuteEditText.setImeHintLocales(locales); |
| } |
| |
| boolean validateInput() { |
| final String hourText = TextUtils.isEmpty(mHourEditText.getText()) |
| ? mHourEditText.getHint().toString() |
| : mHourEditText.getText().toString(); |
| final String minuteText = TextUtils.isEmpty(mMinuteEditText.getText()) |
| ? mMinuteEditText.getHint().toString() |
| : mMinuteEditText.getText().toString(); |
| |
| final boolean inputValid = parseAndSetHourInternal(hourText) |
| && parseAndSetMinuteInternal(minuteText); |
| setError(!inputValid); |
| return inputValid; |
| } |
| |
| void updateSeparator(String separatorText) { |
| mInputSeparatorView.setText(separatorText); |
| } |
| |
| private void setError(boolean enabled) { |
| mErrorShowing = enabled; |
| |
| mErrorLabel.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); |
| mHourLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE); |
| mMinuteLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE); |
| } |
| |
| private void setTimeSet(boolean timeSet) { |
| mTimeSet = mTimeSet || timeSet; |
| } |
| |
| private boolean isTimeSet() { |
| return mTimeSet; |
| } |
| |
| /** |
| * Computes the display value and updates the text of the view. |
| * <p> |
| * This method should be called whenever the current value or display |
| * properties (leading zeroes, max digits) change. |
| */ |
| void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour, |
| boolean hourFormatStartsAtZero) { |
| final String hourFormat = "%d"; |
| final String minuteFormat = "%02d"; |
| |
| mIs24Hour = is24Hour; |
| mHourFormatStartsAtZero = hourFormatStartsAtZero; |
| |
| mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE); |
| |
| if (amOrPm == AM) { |
| mAmPmSpinner.setSelection(0); |
| } else { |
| mAmPmSpinner.setSelection(1); |
| } |
| |
| if (isTimeSet()) { |
| mHourEditText.setText(String.format(hourFormat, localizedHour)); |
| mMinuteEditText.setText(String.format(minuteFormat, minute)); |
| } else { |
| mHourEditText.setHint(String.format(hourFormat, localizedHour)); |
| mMinuteEditText.setHint(String.format(minuteFormat, minute)); |
| } |
| |
| |
| if (mErrorShowing) { |
| validateInput(); |
| } |
| } |
| |
| private boolean parseAndSetHourInternal(String input) { |
| try { |
| final int hour = Integer.parseInt(input); |
| if (!isValidLocalizedHour(hour)) { |
| final int minHour = mHourFormatStartsAtZero ? 0 : 1; |
| final int maxHour = mIs24Hour ? 23 : 11 + minHour; |
| mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour( |
| MathUtils.constrain(hour, minHour, maxHour))); |
| return false; |
| } |
| mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour)); |
| setTimeSet(true); |
| return true; |
| } catch (NumberFormatException e) { |
| // Do nothing since we cannot parse the input. |
| return false; |
| } |
| } |
| |
| private boolean parseAndSetMinuteInternal(String input) { |
| try { |
| final int minutes = Integer.parseInt(input); |
| if (minutes < 0 || minutes > 59) { |
| mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59)); |
| return false; |
| } |
| mListener.onValueChanged(MINUTES, minutes); |
| setTimeSet(true); |
| return true; |
| } catch (NumberFormatException e) { |
| // Do nothing since we cannot parse the input. |
| return false; |
| } |
| } |
| |
| private boolean isValidLocalizedHour(int localizedHour) { |
| final int minHour = mHourFormatStartsAtZero ? 0 : 1; |
| final int maxHour = (mIs24Hour ? 23 : 11) + minHour; |
| return localizedHour >= minHour && localizedHour <= maxHour; |
| } |
| |
| private int getHourOfDayFromLocalizedHour(int localizedHour) { |
| int hourOfDay = localizedHour; |
| if (mIs24Hour) { |
| if (!mHourFormatStartsAtZero && localizedHour == 24) { |
| hourOfDay = 0; |
| } |
| } else { |
| if (!mHourFormatStartsAtZero && localizedHour == 12) { |
| hourOfDay = 0; |
| } |
| if (mAmPmSpinner.getSelectedItemPosition() == 1) { |
| hourOfDay += 12; |
| } |
| } |
| return hourOfDay; |
| } |
| } |