| /* |
| * 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.annotation.UnsupportedAppUsage; |
| 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; |
| |
| @UnsupportedAppUsage |
| 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); |
| } |
| } |