332 lines
11 KiB
Java
332 lines
11 KiB
Java
/*
|
|
* 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.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.Context;
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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);
|
|
}
|
|
}
|