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