1 /* 2 * Copyright (C) 2015 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 17 package com.android.internal.widget; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.content.Context; 21 import android.graphics.Rect; 22 import android.util.AttributeSet; 23 import android.util.StateSet; 24 import android.view.KeyEvent; 25 import android.widget.TextView; 26 27 /** 28 * Extension of TextView that can handle displaying and inputting a range of 29 * numbers. 30 * <p> 31 * Clients of this view should never call {@link #setText(CharSequence)} or 32 * {@link #setHint(CharSequence)} directly. Instead, they should call 33 * {@link #setValue(int)} to modify the currently displayed value. 34 */ 35 public class NumericTextView extends TextView { 36 private static final int RADIX = 10; 37 private static final double LOG_RADIX = Math.log(RADIX); 38 39 private int mMinValue = 0; 40 private int mMaxValue = 99; 41 42 /** Number of digits in the maximum value. */ 43 private int mMaxCount = 2; 44 45 private boolean mShowLeadingZeroes = true; 46 47 private int mValue; 48 49 /** Number of digits entered during editing mode. */ 50 private int mCount; 51 52 /** Used to restore the value after an aborted edit. */ 53 private int mPreviousValue; 54 55 private OnValueChangedListener mListener; 56 57 @UnsupportedAppUsage NumericTextView(Context context, AttributeSet attrs)58 public NumericTextView(Context context, AttributeSet attrs) { 59 super(context, attrs); 60 61 // Generate the hint text color based on disabled state. 62 final int textColorDisabled = getTextColors().getColorForState(StateSet.get(0), 0); 63 setHintTextColor(textColorDisabled); 64 65 setFocusable(true); 66 } 67 68 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)69 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 70 super.onFocusChanged(focused, direction, previouslyFocusedRect); 71 72 if (focused) { 73 mPreviousValue = mValue; 74 mValue = 0; 75 mCount = 0; 76 77 // Transfer current text to hint. 78 setHint(getText()); 79 setText(""); 80 } else { 81 if (mCount == 0) { 82 // No digits were entered, revert to previous value. 83 mValue = mPreviousValue; 84 85 setText(getHint()); 86 setHint(""); 87 } 88 89 // Ensure the committed value is within range. 90 if (mValue < mMinValue) { 91 mValue = mMinValue; 92 } 93 94 setValue(mValue); 95 96 if (mListener != null) { 97 mListener.onValueChanged(this, mValue, true, true); 98 } 99 } 100 } 101 102 /** 103 * Sets the currently displayed value. 104 * <p> 105 * The specified {@code value} must be within the range specified by 106 * {@link #setRange(int, int)} (e.g. between {@link #getRangeMinimum()} 107 * and {@link #getRangeMaximum()}). 108 * 109 * @param value the value to display 110 */ setValue(int value)111 public final void setValue(int value) { 112 if (mValue != value) { 113 mValue = value; 114 115 updateDisplayedValue(); 116 } 117 } 118 119 /** 120 * Returns the currently displayed value. 121 * <p> 122 * If the value is currently being edited, returns the live value which may 123 * not be within the range specified by {@link #setRange(int, int)}. 124 * 125 * @return the currently displayed value 126 */ getValue()127 public final int getValue() { 128 return mValue; 129 } 130 131 /** 132 * Sets the valid range (inclusive). 133 * 134 * @param minValue the minimum valid value (inclusive) 135 * @param maxValue the maximum valid value (inclusive) 136 */ setRange(int minValue, int maxValue)137 public final void setRange(int minValue, int maxValue) { 138 if (mMinValue != minValue) { 139 mMinValue = minValue; 140 } 141 142 if (mMaxValue != maxValue) { 143 mMaxValue = maxValue; 144 mMaxCount = 1 + (int) (Math.log(maxValue) / LOG_RADIX); 145 146 updateMinimumWidth(); 147 updateDisplayedValue(); 148 } 149 } 150 151 /** 152 * @return the minimum value value (inclusive) 153 */ getRangeMinimum()154 public final int getRangeMinimum() { 155 return mMinValue; 156 } 157 158 /** 159 * @return the maximum value value (inclusive) 160 */ getRangeMaximum()161 public final int getRangeMaximum() { 162 return mMaxValue; 163 } 164 165 /** 166 * Sets whether this view shows leading zeroes. 167 * <p> 168 * When leading zeroes are shown, the displayed value will be padded 169 * with zeroes to the width of the maximum value as specified by 170 * {@link #setRange(int, int)} (see also {@link #getRangeMaximum()}. 171 * <p> 172 * For example, with leading zeroes shown, a maximum of 99 and value of 173 * 9 would display "09". A maximum of 100 and a value of 9 would display 174 * "009". With leading zeroes hidden, both cases would show "9". 175 * 176 * @param showLeadingZeroes {@code true} to show leading zeroes, 177 * {@code false} to hide them 178 */ setShowLeadingZeroes(boolean showLeadingZeroes)179 public final void setShowLeadingZeroes(boolean showLeadingZeroes) { 180 if (mShowLeadingZeroes != showLeadingZeroes) { 181 mShowLeadingZeroes = showLeadingZeroes; 182 183 updateDisplayedValue(); 184 } 185 } 186 getShowLeadingZeroes()187 public final boolean getShowLeadingZeroes() { 188 return mShowLeadingZeroes; 189 } 190 191 /** 192 * Computes the display value and updates the text of the view. 193 * <p> 194 * This method should be called whenever the current value or display 195 * properties (leading zeroes, max digits) change. 196 */ updateDisplayedValue()197 private void updateDisplayedValue() { 198 final String format; 199 if (mShowLeadingZeroes) { 200 format = "%0" + mMaxCount + "d"; 201 } else { 202 format = "%d"; 203 } 204 205 // Always use String.format() rather than Integer.toString() 206 // to obtain correctly localized values. 207 setText(String.format(format, mValue)); 208 } 209 210 /** 211 * Computes the minimum width in pixels required to display all possible 212 * values and updates the minimum width of the view. 213 * <p> 214 * This method should be called whenever the maximum value changes. 215 */ updateMinimumWidth()216 private void updateMinimumWidth() { 217 final CharSequence previousText = getText(); 218 int maxWidth = 0; 219 220 for (int i = 0; i < mMaxValue; i++) { 221 setText(String.format("%0" + mMaxCount + "d", i)); 222 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 223 224 final int width = getMeasuredWidth(); 225 if (width > maxWidth) { 226 maxWidth = width; 227 } 228 } 229 230 setText(previousText); 231 setMinWidth(maxWidth); 232 setMinimumWidth(maxWidth); 233 } 234 setOnDigitEnteredListener(OnValueChangedListener listener)235 public final void setOnDigitEnteredListener(OnValueChangedListener listener) { 236 mListener = listener; 237 } 238 getOnDigitEnteredListener()239 public final OnValueChangedListener getOnDigitEnteredListener() { 240 return mListener; 241 } 242 243 @Override onKeyDown(int keyCode, KeyEvent event)244 public boolean onKeyDown(int keyCode, KeyEvent event) { 245 return isKeyCodeNumeric(keyCode) 246 || (keyCode == KeyEvent.KEYCODE_DEL) 247 || super.onKeyDown(keyCode, event); 248 } 249 250 @Override onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)251 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 252 return isKeyCodeNumeric(keyCode) 253 || (keyCode == KeyEvent.KEYCODE_DEL) 254 || super.onKeyMultiple(keyCode, repeatCount, event); 255 } 256 257 @Override onKeyUp(int keyCode, KeyEvent event)258 public boolean onKeyUp(int keyCode, KeyEvent event) { 259 return handleKeyUp(keyCode) 260 || super.onKeyUp(keyCode, event); 261 } 262 handleKeyUp(int keyCode)263 private boolean handleKeyUp(int keyCode) { 264 if (keyCode == KeyEvent.KEYCODE_DEL) { 265 // Backspace removes the least-significant digit, if available. 266 if (mCount > 0) { 267 mValue /= RADIX; 268 mCount--; 269 } 270 } else if (isKeyCodeNumeric(keyCode)) { 271 if (mCount < mMaxCount) { 272 final int keyValue = numericKeyCodeToInt(keyCode); 273 final int newValue = mValue * RADIX + keyValue; 274 if (newValue <= mMaxValue) { 275 mValue = newValue; 276 mCount++; 277 } 278 } 279 } else { 280 return false; 281 } 282 283 final String formattedValue; 284 if (mCount > 0) { 285 // If the user types 01, we should always show the leading 0 even if 286 // getShowLeadingZeroes() is false. Preserve typed leading zeroes by 287 // using the number of digits entered as the format width. 288 formattedValue = String.format("%0" + mCount + "d", mValue); 289 } else { 290 formattedValue = ""; 291 } 292 293 setText(formattedValue); 294 295 if (mListener != null) { 296 final boolean isValid = mValue >= mMinValue; 297 final boolean isFinished = mCount >= mMaxCount || mValue * RADIX > mMaxValue; 298 mListener.onValueChanged(this, mValue, isValid, isFinished); 299 } 300 301 return true; 302 } 303 isKeyCodeNumeric(int keyCode)304 private static boolean isKeyCodeNumeric(int keyCode) { 305 return keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 306 || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 307 || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 308 || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 309 || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9; 310 } 311 numericKeyCodeToInt(int keyCode)312 private static int numericKeyCodeToInt(int keyCode) { 313 return keyCode - KeyEvent.KEYCODE_0; 314 } 315 316 public interface OnValueChangedListener { 317 /** 318 * Called when the value displayed by {@code view} changes. 319 * 320 * @param view the view whose value changed 321 * @param value the new value 322 * @param isValid {@code true} if the value is valid (e.g. within the 323 * range specified by {@link #setRange(int, int)}), 324 * {@code false} otherwise 325 * @param isFinished {@code true} if the no more digits may be entered, 326 * {@code false} if more digits may be entered 327 */ onValueChanged(NumericTextView view, int value, boolean isValid, boolean isFinished)328 void onValueChanged(NumericTextView view, int value, boolean isValid, boolean isFinished); 329 } 330 } 331