blob: 23f258284efa2653f15746c114dc10c74d4e48dd [file] [log] [blame]
/*
* 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;
}
}