/* * Copyright (C) 2008 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.micabytes.gui; import android.content.Context; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.InputFilter; import android.text.InputType; import android.text.Spanned; import android.text.method.NumberKeyListener; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.View.OnLongClickListener; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import com.micabytes.R; import com.micabytes.util.GameLog; import org.jetbrains.annotations.NonNls; import java.util.Locale; /** * This class has been pulled from the Android platform source code, its an internal widget that * hasn't been made public so its included in the project in this fashion for use with the * preferences screen; I have made a few slight modifications to the code here, * * @author micabyte */ public final class NumberPicker extends LinearLayout implements OnClickListener, OnFocusChangeListener, OnLongClickListener { private static final String TAG = NumberPicker.class.getName(); private static final int DEFAULT_MAX = 200; private static final int DEFAULT_MIN = 0; public static final int DEFAULT_SPEED = 300; @SuppressWarnings("InterfaceNeverImplemented") private interface OnChangedListener { void onChanged(NumberPicker picker, int oldVal, int newVal); } @SuppressWarnings("InterfaceNeverImplemented") private interface Formatter { String toString(int value); } private final Handler handler; private final Runnable runnable = new Runnable() { @Override public void run() { if (increment) { changeCurrent(currentValue + incrementSize); handler.postDelayed(this, speed); } else if (decrement) { changeCurrent(currentValue - incrementSize); handler.postDelayed(this, speed); } } }; private final EditText text; private final InputFilter numberInputFilter; private String[] displayedValues; private int startValue; private int endValue; private int currentValue; private int previous; private OnChangedListener listener; @SuppressWarnings("unused") private Formatter formatter; private long speed = DEFAULT_SPEED; private boolean increment; private boolean decrement; private int incrementSize = 1; public NumberPicker(Context context) { this(context, null); } public NumberPicker(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } @SuppressWarnings("ThisEscapedInObjectConstruction") public NumberPicker(Context context, @Nullable AttributeSet attrs, @SuppressWarnings("UnusedParameters") int defStyle) { super(context, attrs); setOrientation(VERTICAL); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.number_picker, this, true); handler = new Handler(); InputFilter inputFilter = new NumberPickerInputFilter(); numberInputFilter = new NumberRangeKeyListener(); incrementButton = (NumberPickerButton) findViewById(R.id.increment); incrementButton.setOnClickListener(this); incrementButton.setOnLongClickListener(this); incrementButton.setNumberPicker(this); decrementButton = (NumberPickerButton) findViewById(R.id.decrement); decrementButton.setOnClickListener(this); decrementButton.setOnLongClickListener(this); decrementButton.setNumberPicker(this); text = (EditText) findViewById(R.id.timepicker_input); text.setOnFocusChangeListener(this); text.setFilters(new InputFilter[]{inputFilter}); text.setRawInputType(InputType.TYPE_CLASS_NUMBER); if (!isEnabled()) { setEnabled(false); } startValue = DEFAULT_MIN; endValue = DEFAULT_MAX; } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); incrementButton.setEnabled(enabled); decrementButton.setEnabled(enabled); text.setEnabled(enabled); } @SuppressWarnings("unused") public void setListener(OnChangedListener l) { listener = l; } /** * Set the range of numbers allowed for the number picker. The currentValue value will be automatically * set to the startValue. * * @param i the i of the range (inclusive) */ @SuppressWarnings("unused") public void setRange(int i) { GameLog.d(TAG, "Set range of dialog " + 1 + " to " + i); startValue = 1; endValue = i; currentValue = 1; updateView(); } /** * Set the range of numbers allowed for the number picker. The currentValue value will be automatically * set to the startValue. Also provide a mapping for values used to display to the user. * * @param st the startValue of the range (inclusive) * @param ed the endValue of the range (inclusive) * @param dv the values displayed to the user. */ @SuppressWarnings({"unused", "MethodCanBeVariableArityMethod", "AssignmentToCollectionOrArrayFieldFromParameter"}) public void setRange(int st, int ed, @Nullable String[] dv) { displayedValues = dv; startValue = st; endValue = ed; currentValue = st; updateView(); } @SuppressWarnings("unused") public void setIncrementSize(int n) { incrementSize = n; } public void setCurrentValue(int val) { currentValue = val; updateView(); } /** * The speed (in milliseconds) at which the numbers will scroll when the the +/- buttons are * longpressed. Default is 300ms. */ public void setSpeed(long spd) { speed = spd; } @Override public void onClick(View v) { validateInput(text); if (!text.hasFocus()) text.requestFocus(); // now perform the increment/decrement if (R.id.increment == v.getId()) { changeCurrent(currentValue + incrementSize); } else if (R.id.decrement == v.getId()) { changeCurrent(currentValue - incrementSize); } } private String formatNumber(int value) { return (formatter != null) ? formatter.toString(value) : String.valueOf(value); } private void changeCurrent(int c) { int cur = c; // Wrap around the values if we go past the startValue or endValue if (cur > endValue) { cur = startValue; } else if (cur < startValue) { cur = endValue; } previous = currentValue; currentValue = cur; notifyChange(); updateView(); } private void notifyChange() { if (listener != null) { listener.onChanged(this, previous, currentValue); } } private void updateView() { /* If we don't have displayed values then use the * currentValue number else find the correct value in the * displayed values for the currentValue number. */ text.getText().clear(); if (displayedValues == null) { text.append(formatNumber(currentValue)); } else { text.append(displayedValues[currentValue - startValue]); } text.setSelection(text.getText().length()); } private void validateCurrentView(CharSequence str) { int val = getSelectedPos(str.toString()); if ((val >= startValue) && (val <= endValue)) { if (currentValue != val) { previous = currentValue; currentValue = val; notifyChange(); } } updateView(); } @Override public void onFocusChange(View v, boolean hasFocus) { /* When focus is lost check that the text field * has valid values. */ if (!hasFocus) { validateInput(v); } } private void validateInput(View v) { @NonNls String str = String.valueOf(((TextView) v).getText()); if (str != null && str.isEmpty()) { // Restore to the old value as we don't allow empty values updateView(); } else { // Check the new value and ensure it's in range if (str != null) validateCurrentView(str); } } /** * We startValue the long click here but rely on the {@link NumberPickerButton} to inform us when the * long click has ended. */ @Override public boolean onLongClick(View v) { /* The text view may still have focus so clear it's focus which will * trigger the on focus changed and any typed values to be pulled. */ text.clearFocus(); if (R.id.increment == v.getId()) { increment = true; handler.post(runnable); } else if (R.id.decrement == v.getId()) { decrement = true; handler.post(runnable); } return true; } public void cancelIncrement() { increment = false; } public void cancelDecrement() { decrement = false; } private static final char[] DIGIT_CHARACTERS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; private final NumberPickerButton incrementButton; private final NumberPickerButton decrementButton; private final class NumberPickerInputFilter implements InputFilter { @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { if (displayedValues == null) { return numberInputFilter.filter(source, start, end, dest, dstart, dend); } @NonNls CharSequence filtered = String.valueOf(source.subSequence(start, end)); @NonNls String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + dest.subSequence(dend, dest.length()); String str = String.valueOf(result).toLowerCase(Locale.US); for (String val : displayedValues) { val = val.toLowerCase(Locale.US); if (val.startsWith(str)) { return filtered; } } return ""; } } private final class NumberRangeKeyListener extends NumberKeyListener { // This doesn't allow for range limits when controlled by a // soft input method! @SuppressWarnings("MethodReturnAlwaysConstant") @Override public int getInputType() { return InputType.TYPE_CLASS_NUMBER; } @Override protected char[] getAcceptedChars() { return DIGIT_CHARACTERS; } @Override public CharSequence filter(@NonNull CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { @NonNls CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); if (filtered == null) { filtered = source.subSequence(start, end); } @NonNls String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + dest.subSequence(dend, dest.length()); if (result.isEmpty()) { return result; } int val = getSelectedPos(result); /* Ensure the user can't type in a value greater * than the max allowed. We have to allow less than min * as the user might want to delete some numbers * and then type a new number. */ if (val > endValue) { return ""; } return filtered; } } private int getSelectedPos(String str) { if (displayedValues == null) { try { return Integer.parseInt(str); } catch (NumberFormatException ignored) { // Invalid number format return startValue; } } for (int i = 0; i < displayedValues.length; i++) { /* Don't force the user to type in jan when ja will do */ String stl = str.toLowerCase(Locale.US); if (displayedValues[i].toLowerCase(Locale.US).startsWith(stl)) { return startValue + i; } } /* The user might have typed in a number into the month field i.e. * 10 instead of OCT so support that too. */ try { return Integer.parseInt(str); } catch (NumberFormatException ignored) { /* Ignore as if it's not a number we don't care */ } return startValue; } /** * @return the currentValue value. */ public int getCurrentValue() { try { currentValue = Integer.parseInt(String.valueOf(((TextView) findViewById(R.id.timepicker_input)).getText())); } catch (NumberFormatException ignored) { return 0; } return currentValue; } }