/* * Copyright (C) 2016 Phillip Hsu * * 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.philliphsu.bottomsheetpickers.time.numberpad; import android.content.Context; import android.content.res.ColorStateList; import android.support.annotation.CallSuper; import android.support.annotation.ColorInt; import android.support.annotation.LayoutRes; import android.support.v4.content.ContextCompat; import android.support.v7.widget.GridLayout; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import com.philliphsu.bottomsheetpickers.R; import com.philliphsu.bottomsheetpickers.Utils; import java.util.Arrays; /* * TODO: Is NumberPadTimePicker the only subclass? If so, why do we need this * superclass? If we move the contents of this class to NumberPadTimePicker, * the implementation of setTheme() would make more sense. */ abstract class GridLayoutNumberPad extends GridLayout implements View.OnClickListener { // TODO: change to private? protected static final int UNMODIFIED = -1; private static final int COLUMNS = 3; private int[] mInput; private int mCount = 0; private OnInputChangeListener mOnInputChangeListener; private ColorStateList mTextColors; int mAccentColor; private boolean mAccentColorSetAtRuntime; private final TextView[] mButtons = new TextView[10]; /** * Informs clients how to output the digits inputted into this numpad. */ public interface OnInputChangeListener { /** * @param newStr the new value of the input formatted as a * String after a digit insertion */ void onDigitInserted(String newStr); /** * @param newStr the new value of the input formatted as a * String after a digit deletion */ void onDigitDeleted(String newStr); void onDigitsCleared(); } public GridLayoutNumberPad(Context context) { this(context, null); } public GridLayoutNumberPad(Context context, AttributeSet attrs) { super(context, attrs); init(); } void setTheme(Context context, boolean themeDark) { // Since the Dialog class already set the background color of its entire view tree, // our background is already colored. Why did we set it in the Dialog class? Because // we use margins around the numpad, and if we had instead set the background on // this numpad here, the margins will not be colored. Why not use padding instead // of margins? It turns out we tried that--replacing each margin attribute // with the padding counterpart--but we lost the pre-21 FAB inherent bottom margin. // The buttons are actually of type Button, but we kept references // to them as TextViews... which is fine since TextView is the superclass // of Button. mTextColors = ContextCompat.getColorStateList(context, themeDark? R.color.bsp_numeric_keypad_button_text_dark : R.color.bsp_numeric_keypad_button_text); if (!mAccentColorSetAtRuntime) { // AFAIK, the only way to get the user's accent color is programmatically, // because it is uniquely defined in their app's theme. It is not possible // for us to reference that via XML (i.e. with ?colorAccent or similar), // which happens at compile time. // TOneverDO: Use any other Context to retrieve the accent color. We must use // the Context param passed to us, because we know this context to be // NumberPadTimePickerDialog.getContext(), which is equivalent to // NumberPadTimePickerDialog.getActivity(). It is from that Activity where we // get its theme's colorAccent. mAccentColor = Utils.getThemeAccentColor(context); } for (TextView b : mButtons) { setTextColor(b); Utils.setColorControlHighlight(b, mAccentColor); } } void setAccentColor(@ColorInt int color) { mAccentColor = color; mAccentColorSetAtRuntime = true; } void setTextColor(TextView view) { view.setTextColor(mTextColors); } /** * @return the number of digits we can input */ public abstract int capacity(); @LayoutRes protected abstract int contentLayout(); public final void setOnInputChangeListener(OnInputChangeListener onInputChangeListener) { mOnInputChangeListener = onInputChangeListener; } /** * Provided only for subclasses so they can retrieve the registered listener * and fire any custom OnInputChange events they may have defined. */ protected final OnInputChangeListener getOnInputChangeListener() { return mOnInputChangeListener; } @CallSuper protected void enable(int lowerLimitInclusive, int upperLimitExclusive) { if (lowerLimitInclusive < 0 || upperLimitExclusive > mButtons.length) throw new IndexOutOfBoundsException("Upper limit out of range"); for (int i = 0; i < mButtons.length; i++) mButtons[i].setEnabled(i >= lowerLimitInclusive && i < upperLimitExclusive); } protected final int valueAt(int index) { return mInput[index]; } /** * @return a defensive copy of the internal array of inputted digits */ protected final int[] getDigits() { int[] digits = new int[mInput.length]; System.arraycopy(mInput, 0, digits, 0, mInput.length); return digits; } /** * @return the number of digits inputted */ public final int count() { return mCount; } /** * @return the integer represented by the inputted digits */ protected final int getInput() { return Integer.parseInt(getInputString()); } private String getInputString() { String currentInput = ""; for (int i : mInput) if (i != UNMODIFIED) currentInput += i; return currentInput; } public void delete() { /* if (mCount - 1 >= 0) { mInput[--mCount] = UNMODIFIED; } onDigitDeleted(getInputString()); */ delete(mCount); } // TODO: Why do we need this? @Deprecated public void delete(int at) { if (at - 1 >= 0) { mInput[at - 1] = UNMODIFIED; mCount--; onDigitDeleted(getInputString()); } } public boolean clear() { Arrays.fill(mInput, UNMODIFIED); mCount = 0; onDigitsCleared(); return true; } /** * Forwards the provided String to the assigned * {@link OnInputChangeListener OnInputChangeListener} * after a digit insertion. By default, the String * forwarded is just the String value of the inserted digit. * @see #onClick(View) * @param newDigit the formatted String that should be displayed */ @CallSuper protected void onDigitInserted(String newDigit) { if (mOnInputChangeListener != null) { mOnInputChangeListener.onDigitInserted(newDigit); } } /** * Forwards the provided String to the assigned * {@link OnInputChangeListener OnInputChangeListener} * after a digit deletion. By default, the String * forwarded is {@link #getInputString()}. * @param newStr the formatted String that should be displayed */ @CallSuper protected void onDigitDeleted(String newStr) { if (mOnInputChangeListener != null) { mOnInputChangeListener.onDigitDeleted(newStr); } } /** * Forwards a {@code onDigitsCleared()} event to the assigned * {@link OnInputChangeListener OnInputChangeListener}. */ @CallSuper protected void onDigitsCleared() { if (mOnInputChangeListener != null) { mOnInputChangeListener.onDigitsCleared(); } } /** * Inserts as many of the digits in the given sequence * into the input as possible. At the end, if any digits * were inserted, this calls {@link #onDigitInserted(String)} * with the String value of those digits. */ protected final void insertDigits(int... digits) { if (digits == null) return; String newDigits = ""; for (int d : digits) { if (mCount == mInput.length) break; if (d == UNMODIFIED) continue; mInput[mCount++] = d; newDigits += d; } if (!newDigits.isEmpty()) { // By only calling this once after making // the insertions, we skip all of the // intermediate callbacks. onDigitInserted(newDigits); } } @Override public final void onClick(View view) { if (mCount < mInput.length) { String textNum = ((TextView) view).getText().toString(); insertDigits(Integer.parseInt(textNum)); } } private void init() { setAlignmentMode(ALIGN_BOUNDS); setColumnCount(COLUMNS); View.inflate(getContext(), contentLayout(), this); mButtons[0] = (TextView) findViewById(R.id.bsp_zero); mButtons[1] = (TextView) findViewById(R.id.bsp_one); mButtons[2] = (TextView) findViewById(R.id.bsp_two); mButtons[3] = (TextView) findViewById(R.id.bsp_three); mButtons[4] = (TextView) findViewById(R.id.bsp_four); mButtons[5] = (TextView) findViewById(R.id.bsp_five); mButtons[6] = (TextView) findViewById(R.id.bsp_six); mButtons[7] = (TextView) findViewById(R.id.bsp_seven); mButtons[8] = (TextView) findViewById(R.id.bsp_eight); mButtons[9] = (TextView) findViewById(R.id.bsp_nine); for (TextView tv : mButtons) { tv.setOnClickListener(this); } // If capacity() < 0, we let the system throw the exception. mInput = new int[capacity()]; Arrays.fill(mInput, UNMODIFIED); } }