/* * Copyright (C) 2015 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 com.android.internal.widget; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.StateSet; import android.view.KeyEvent; import android.widget.TextView; /** * Extension of TextView that can handle displaying and inputting a range of * numbers. * <p> * Clients of this view should never call {@link #setText(CharSequence)} or * {@link #setHint(CharSequence)} directly. Instead, they should call * {@link #setValue(int)} to modify the currently displayed value. */ public class NumericTextView extends TextView { private static final int RADIX = 10; private static final double LOG_RADIX = Math.log(RADIX); private int mMinValue = 0; private int mMaxValue = 99; /** Number of digits in the maximum value. */ private int mMaxCount = 2; private boolean mShowLeadingZeroes = true; private int mValue; /** Number of digits entered during editing mode. */ private int mCount; /** Used to restore the value after an aborted edit. */ private int mPreviousValue; private OnValueChangedListener mListener; public NumericTextView(Context context, AttributeSet attrs) { super(context, attrs); // Generate the hint text color based on disabled state. final int textColorDisabled = getTextColors().getColorForState(StateSet.get(0), 0); setHintTextColor(textColorDisabled); setFocusable(true); } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); if (focused) { mPreviousValue = mValue; mValue = 0; mCount = 0; // Transfer current text to hint. setHint(getText()); setText(""); } else { if (mCount == 0) { // No digits were entered, revert to previous value. mValue = mPreviousValue; setText(getHint()); setHint(""); } // Ensure the committed value is within range. if (mValue < mMinValue) { mValue = mMinValue; } setValue(mValue); if (mListener != null) { mListener.onValueChanged(this, mValue, true, true); } } } /** * Sets the currently displayed value. * <p> * The specified {@code value} must be within the range specified by * {@link #setRange(int, int)} (e.g. between {@link #getRangeMinimum()} * and {@link #getRangeMaximum()}). * * @param value the value to display */ public final void setValue(int value) { if (mValue != value) { mValue = value; updateDisplayedValue(); } } /** * Returns the currently displayed value. * <p> * If the value is currently being edited, returns the live value which may * not be within the range specified by {@link #setRange(int, int)}. * * @return the currently displayed value */ public final int getValue() { return mValue; } /** * Sets the valid range (inclusive). * * @param minValue the minimum valid value (inclusive) * @param maxValue the maximum valid value (inclusive) */ public final void setRange(int minValue, int maxValue) { if (mMinValue != minValue) { mMinValue = minValue; } if (mMaxValue != maxValue) { mMaxValue = maxValue; mMaxCount = 1 + (int) (Math.log(maxValue) / LOG_RADIX); updateMinimumWidth(); updateDisplayedValue(); } } /** * @return the minimum value value (inclusive) */ public final int getRangeMinimum() { return mMinValue; } /** * @return the maximum value value (inclusive) */ public final int getRangeMaximum() { return mMaxValue; } /** * Sets whether this view shows leading zeroes. * <p> * When leading zeroes are shown, the displayed value will be padded * with zeroes to the width of the maximum value as specified by * {@link #setRange(int, int)} (see also {@link #getRangeMaximum()}. * <p> * For example, with leading zeroes shown, a maximum of 99 and value of * 9 would display "09". A maximum of 100 and a value of 9 would display * "009". With leading zeroes hidden, both cases would show "9". * * @param showLeadingZeroes {@code true} to show leading zeroes, * {@code false} to hide them */ public final void setShowLeadingZeroes(boolean showLeadingZeroes) { if (mShowLeadingZeroes != showLeadingZeroes) { mShowLeadingZeroes = showLeadingZeroes; updateDisplayedValue(); } } public final boolean getShowLeadingZeroes() { return mShowLeadingZeroes; } /** * 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. */ private void updateDisplayedValue() { final String format; if (mShowLeadingZeroes) { format = "%0" + mMaxCount + "d"; } else { format = "%d"; } // Always use String.format() rather than Integer.toString() // to obtain correctly localized values. setText(String.format(format, mValue)); } /** * Computes the minimum width in pixels required to display all possible * values and updates the minimum width of the view. * <p> * This method should be called whenever the maximum value changes. */ private void updateMinimumWidth() { final CharSequence previousText = getText(); int maxWidth = 0; for (int i = 0; i < mMaxValue; i++) { setText(String.format("%0" + mMaxCount + "d", i)); measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); final int width = getMeasuredWidth(); if (width > maxWidth) { maxWidth = width; } } setText(previousText); setMinWidth(maxWidth); setMinimumWidth(maxWidth); } public final void setOnDigitEnteredListener(OnValueChangedListener listener) { mListener = listener; } public final OnValueChangedListener getOnDigitEnteredListener() { return mListener; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return isKeyCodeNumeric(keyCode) || (keyCode == KeyEvent.KEYCODE_DEL) || super.onKeyDown(keyCode, event); } @Override public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { return isKeyCodeNumeric(keyCode) || (keyCode == KeyEvent.KEYCODE_DEL) || super.onKeyMultiple(keyCode, repeatCount, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return handleKeyUp(keyCode) || super.onKeyUp(keyCode, event); } private boolean handleKeyUp(int keyCode) { if (keyCode == KeyEvent.KEYCODE_DEL) { // Backspace removes the least-significant digit, if available. if (mCount > 0) { mValue /= RADIX; mCount--; } } else if (isKeyCodeNumeric(keyCode)) { if (mCount < mMaxCount) { final int keyValue = numericKeyCodeToInt(keyCode); final int newValue = mValue * RADIX + keyValue; if (newValue <= mMaxValue) { mValue = newValue; mCount++; } } } else { return false; } final String formattedValue; if (mCount > 0) { // If the user types 01, we should always show the leading 0 even if // getShowLeadingZeroes() is false. Preserve typed leading zeroes by // using the number of digits entered as the format width. formattedValue = String.format("%0" + mCount + "d", mValue); } else { formattedValue = ""; } setText(formattedValue); if (mListener != null) { final boolean isValid = mValue >= mMinValue; final boolean isFinished = mCount >= mMaxCount || mValue * RADIX > mMaxValue; mListener.onValueChanged(this, mValue, isValid, isFinished); } return true; } private static boolean isKeyCodeNumeric(int keyCode) { return keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9; } private static int numericKeyCodeToInt(int keyCode) { return keyCode - KeyEvent.KEYCODE_0; } public interface OnValueChangedListener { /** * Called when the value displayed by {@code view} changes. * * @param view the view whose value changed * @param value the new value * @param isValid {@code true} if the value is valid (e.g. within the * range specified by {@link #setRange(int, int)}), * {@code false} otherwise * @param isFinished {@code true} if the no more digits may be entered, * {@code false} if more digits may be entered */ void onValueChanged(NumericTextView view, int value, boolean isValid, boolean isFinished); } }