Home | History | Annotate | Download | only in widget
      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 
     17 package com.android.internal.widget;
     18 
     19 import android.content.Context;
     20 import android.graphics.Rect;
     21 import android.util.AttributeSet;
     22 import android.util.StateSet;
     23 import android.view.KeyEvent;
     24 import 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  */
     34 public 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