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