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