/*
* 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);
}
}