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