package com.michaelmuenzer.android.scrollablennumberpicker;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.widget.TextViewCompat;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
public class ScrollableNumberPicker extends LinearLayout {
private final static int INVALID_RES = -1;
private final static float SLOWING_FACTOR = 1.25f;
private final static int MIN_UPDATE_INTERVAL_MS = 50;
@DrawableRes
private int downIcon = R.drawable.ic_arrow_down;
@DrawableRes
private int upIcon = R.drawable.ic_arrow_up;
@DrawableRes
private int leftIcon = R.drawable.ic_arrow_left;
@DrawableRes
private int rightIcon = R.drawable.ic_arrow_right;
private int mValue;
private int mMaxValue;
private int mMinValue;
private int mStepSize;
private float mValueTextSize;
private int mValueTextColor;
private int mValueTextAppearanceResId;
private boolean mScrollEnabled;
private int mUpdateIntervalMillis;
private float mButtonTouchScaleFactor;
private int mOrientation;
private ColorStateList mButtonColorStateList;
private int mValueMarginStart;
private int mValueMarginEnd;
private ImageView mMinusButton;
private ImageView mPlusButton;
private TextView mValueTextView;
private int mButtonPaddingLeft;
private int mButtonPaddingRight;
private int mButtonPaddingTop;
private int mButtonPaddingBottom;
private boolean mAutoIncrement;
private boolean mAutoDecrement;
private Handler mUpdateIntervalHandler;
private ScrollableNumberPickerListener mListener;
public ScrollableNumberPicker(Context context) {
super(context);
init(context, null);
}
public ScrollableNumberPicker(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public ScrollableNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@SuppressWarnings("unused")
public ImageView getButtonMinusView() {
return mMinusButton;
}
@SuppressWarnings("unused")
public ImageView getButtonPlusView() {
return mPlusButton;
}
@SuppressWarnings("unused")
public boolean isScrollEnabled() {
return mScrollEnabled;
}
@SuppressWarnings("unused")
public int getUpdateIntervalMillis() {
return mUpdateIntervalMillis;
}
@SuppressWarnings("unused")
public float getButtonTouchScaleFactor() {
return mButtonTouchScaleFactor;
}
@SuppressWarnings("unused")
public int getOrientation() {
return mOrientation;
}
@SuppressWarnings("unused")
public ColorStateList getButtonColorStateList() {
return mButtonColorStateList;
}
@SuppressWarnings("unused")
public int getValueMarginStart() {
return mValueMarginStart;
}
@SuppressWarnings("unused")
public int getValueMarginEnd() {
return mValueMarginEnd;
}
@SuppressWarnings("unused")
public TextView getValueView() {
return mValueTextView;
}
@SuppressWarnings("unused")
public int getValue() {
return mValue;
}
@SuppressWarnings("WeakerAccess")
public void setValue(int value) {
if (value > mMaxValue) {
value = mMaxValue;
}
if (value < mMinValue) {
value = mMinValue;
}
mValue = value;
setValue();
}
private void setValue() {
mValueTextView.setText(String.valueOf(mValue));
if (mListener != null) {
mListener.onNumberPicked(mValue);
}
}
private void initValueView() {
mValueTextView = (TextView) findViewById(R.id.text_value);
if (mValueTextAppearanceResId != INVALID_RES) {
TextViewCompat.setTextAppearance(mValueTextView, mValueTextAppearanceResId);
}
if (mValueTextColor != INVALID_RES) {
mValueTextView.setTextColor(mValueTextColor);
}
if (mValueTextSize != INVALID_RES) {
mValueTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mValueTextSize);
}
LinearLayout.LayoutParams layoutParams = (LayoutParams) mValueTextView.getLayoutParams();
if (mOrientation == HORIZONTAL) {
layoutParams.setMargins(mValueMarginStart, 0, mValueMarginEnd, 0);
} else {
layoutParams.setMargins(0, mValueMarginStart, 0, mValueMarginEnd);
}
mValueTextView.setLayoutParams(layoutParams);
setValue();
}
@SuppressWarnings("unused")
public int getMaxValue() {
return mMaxValue;
}
@SuppressWarnings("unused")
public void setMaxValue(int maxValue) {
mMaxValue = maxValue;
if (maxValue < mValue) {
mValue = maxValue;
setValue();
}
}
@SuppressWarnings("unused")
public int getMinValue() {
return mMinValue;
}
@SuppressWarnings("unused")
public void setMinValue(int minValue) {
mMinValue = minValue;
if (minValue > mValue) {
mValue = minValue;
setValue();
}
}
@SuppressWarnings("unused")
public int getStepSize() {
return mStepSize;
}
@SuppressWarnings("unused")
public void setStepSize(int stepSize) {
mStepSize = stepSize;
}
@SuppressWarnings("unused")
public long getOnLongPressUpdateInterval() {
return mUpdateIntervalMillis;
}
@SuppressWarnings("unused")
public void setOnLongPressUpdateInterval(int intervalMillis) {
if (intervalMillis < MIN_UPDATE_INTERVAL_MS) {
intervalMillis = MIN_UPDATE_INTERVAL_MS;
}
mUpdateIntervalMillis = intervalMillis;
}
@SuppressWarnings("unused")
public void setListener(ScrollableNumberPickerListener listener) {
mListener = listener;
}
public boolean handleKeyEvent(int keyCode, KeyEvent event) {
int eventAction = event.getAction();
if (eventAction == KeyEvent.ACTION_DOWN) {
if (mOrientation == HORIZONTAL) {
if (keyCode == KEYCODE_DPAD_LEFT) {
if (event.getRepeatCount() == 0) {
scaleImageViewDrawable(mMinusButton, mButtonTouchScaleFactor);
}
decrement();
return true;
} else if (keyCode == KEYCODE_DPAD_RIGHT) {
if (event.getRepeatCount() == 0) {
scaleImageViewDrawable(mPlusButton, mButtonTouchScaleFactor);
}
increment();
return true;
}
} else {
if (keyCode == KEYCODE_DPAD_UP) {
if (event.getRepeatCount() == 0) {
scaleImageViewDrawable(mPlusButton, mButtonTouchScaleFactor);
}
increment();
return true;
} else if (keyCode == KEYCODE_DPAD_DOWN) {
if (event.getRepeatCount() == 0) {
scaleImageViewDrawable(mMinusButton, mButtonTouchScaleFactor);
}
decrement();
return true;
}
}
} else if (eventAction == KeyEvent.ACTION_UP) {
if (mOrientation == HORIZONTAL) {
if (keyCode == KEYCODE_DPAD_LEFT) {
setButtonMinusImage();
return true;
} else if (keyCode == KEYCODE_DPAD_RIGHT) {
setButtonPlusImage();
return true;
}
} else {
if (keyCode == KEYCODE_DPAD_UP) {
setButtonPlusImage();
return true;
} else if (keyCode == KEYCODE_DPAD_DOWN) {
setButtonMinusImage();
return true;
}
}
}
return false;
}
private void init(Context context, AttributeSet attrs) {
if (isInEditMode()) {
return;
}
LayoutInflater layoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layoutInflater.inflate(R.layout.number_picker, this);
TypedArray typedArray =
context.obtainStyledAttributes(attrs, R.styleable.ScrollableNumberPicker);
Resources res = getResources();
downIcon = typedArray.getResourceId(R.styleable.ScrollableNumberPicker_snp_buttonIconDown, downIcon);
upIcon = typedArray.getResourceId(R.styleable.ScrollableNumberPicker_snp_buttonIconUp, upIcon);
leftIcon = typedArray.getResourceId(R.styleable.ScrollableNumberPicker_snp_buttonIconLeft, leftIcon);
rightIcon = typedArray.getResourceId(R.styleable.ScrollableNumberPicker_snp_buttonIconRight, rightIcon);
mMinValue = typedArray.getInt(R.styleable.ScrollableNumberPicker_snp_minValue,
res.getInteger(R.integer.default_minValue));
mMaxValue = typedArray.getInt(R.styleable.ScrollableNumberPicker_snp_maxValue,
res.getInteger(R.integer.default_maxValue));
mStepSize = typedArray.getInt(R.styleable.ScrollableNumberPicker_snp_stepSize,
res.getInteger(R.integer.default_stepSize));
mUpdateIntervalMillis = typedArray.getInt(R.styleable.ScrollableNumberPicker_snp_updateInterval,
res.getInteger(R.integer.default_updateInterval));
mOrientation = typedArray.getInt(R.styleable.ScrollableNumberPicker_snp_orientation,
LinearLayout.HORIZONTAL);
mValue = typedArray.getInt(R.styleable.ScrollableNumberPicker_snp_value,
res.getInteger(R.integer.default_value));
mValueTextSize = typedArray.getDimension(R.styleable.ScrollableNumberPicker_snp_value_text_size,
INVALID_RES);
mValueTextColor = typedArray.getColor(R.styleable.ScrollableNumberPicker_snp_value_text_color,
INVALID_RES);
mValueTextAppearanceResId = typedArray.getResourceId(R.styleable.ScrollableNumberPicker_snp_value_text_appearance, INVALID_RES);
mScrollEnabled = typedArray.getBoolean(R.styleable.ScrollableNumberPicker_snp_scrollEnabled,
res.getBoolean(R.bool.default_scrollEnabled));
mButtonColorStateList = ContextCompat.getColorStateList(context, typedArray.getResourceId(R.styleable.ScrollableNumberPicker_snp_buttonBackgroundTintSelector, R.color.btn_tint_selector));
mValueMarginStart = (int) typedArray.getDimension(R.styleable.ScrollableNumberPicker_snp_valueMarginStart, res.getDimension(R.dimen.default_value_margin_start));
mValueMarginEnd = (int) typedArray.getDimension(R.styleable.ScrollableNumberPicker_snp_valueMarginStart, res.getDimension(R.dimen.default_value_margin_end));
mButtonPaddingLeft = (int) typedArray.getDimension(R.styleable.ScrollableNumberPicker_snp_buttonPaddingLeft, res.getDimension(R.dimen.default_button_padding_left));
mButtonPaddingRight = (int) typedArray.getDimension(R.styleable.ScrollableNumberPicker_snp_buttonPaddingRight, res.getDimension(R.dimen.default_button_padding_right));
mButtonPaddingTop = (int) typedArray.getDimension(R.styleable.ScrollableNumberPicker_snp_buttonPaddingTop, res.getDimension(R.dimen.default_button_padding_top));
mButtonPaddingBottom = (int) typedArray.getDimension(R.styleable.ScrollableNumberPicker_snp_buttonPaddingBottom, res.getDimension(R.dimen.default_button_padding_bottom));
TypedValue outValue = new TypedValue();
res.getValue(R.dimen.default_button_scale_factor, outValue, true);
float defaultValue = outValue.getFloat();
mButtonTouchScaleFactor = typedArray.getFloat(R.styleable.ScrollableNumberPicker_snp_buttonTouchScaleFactor, defaultValue);
typedArray.recycle();
initViews();
mAutoIncrement = false;
mAutoDecrement = false;
mUpdateIntervalHandler = new Handler();
}
private void initViews() {
setOrientation(mOrientation);
setGravity(Gravity.CENTER);
initValueView();
initButtonPlusView();
initButtonMinusView();
if (mScrollEnabled) {
setOnTouchListener(new OnTouchListener() {
private float lastX = 0.0f;
private float lastY = 0.0f;
private final int scrollOffsetPx = getResources().getDimensionPixelSize(R.dimen.default_scroll_offset);
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
float currentX = motionEvent.getX();
float currentY = motionEvent.getY();
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = currentX;
lastY = currentY;
break;
case MotionEvent.ACTION_MOVE:
if (mOrientation == HORIZONTAL) {
float moveDeltaX = currentX - lastX;
if (moveDeltaX > scrollOffsetPx) {
increment();
break;
} else if ((scrollOffsetPx * -1) > moveDeltaX) {
decrement();
break;
}
} else {
float moveDeltaY = currentY - lastY;
if (moveDeltaY > scrollOffsetPx) {
decrement();
break;
} else if ((scrollOffsetPx * -1) > moveDeltaY) {
increment();
break;
}
}
lastX = currentX;
lastY = currentY;
break;
case MotionEvent.ACTION_UP:
int numberOfRuns;
int singleRunLength = getResources().getDimensionPixelSize(R.dimen.default_scroll_repeat_length);
if (mOrientation == HORIZONTAL) {
float moveDeltaX = currentX - lastX;
if (moveDeltaX > 0) {
mAutoIncrement = true;
} else {
mAutoDecrement = true;
}
numberOfRuns = (int) (Math.abs(moveDeltaX) / singleRunLength);
} else {
float moveDeltaY = currentY - lastY;
if (moveDeltaY > 0) {
mAutoDecrement = true;
} else {
mAutoIncrement = true;
}
numberOfRuns = (int) (Math.abs(moveDeltaY) / singleRunLength);
}
mUpdateIntervalHandler.post(new RepeatSlowingRunnable(numberOfRuns, mUpdateIntervalMillis));
break;
default:
return false;
}
return true;
}
});
}
}
private void initButtonPlusView() {
setButtonPlusImage();
mPlusButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
increment();
}
});
mPlusButton.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View v) {
mAutoIncrement = true;
mUpdateIntervalHandler.post(new RepeatRunnable());
return false;
}
});
mPlusButton.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
scaleImageViewDrawable(mPlusButton, mButtonTouchScaleFactor);
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (mAutoIncrement) {
mAutoIncrement = false;
}
setButtonPlusImage();
}
return false;
}
});
}
private void initButtonMinusView() {
setButtonMinusImage();
mMinusButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
decrement();
}
});
mMinusButton.setOnLongClickListener(new View.OnLongClickListener() {
public boolean onLongClick(View v) {
mAutoDecrement = true;
mUpdateIntervalHandler.post(new RepeatRunnable());
return false;
}
});
mMinusButton.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
scaleImageViewDrawable(mMinusButton, mButtonTouchScaleFactor);
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (mAutoDecrement) {
mAutoDecrement = false;
}
setButtonMinusImage();
}
return false;
}
});
}
private void setButtonPlusImage() {
if (mOrientation == LinearLayout.VERTICAL) {
mPlusButton = (ImageView) findViewById(R.id.button_increase);
mPlusButton.setImageResource(upIcon);
} else if (mOrientation == LinearLayout.HORIZONTAL) {
mPlusButton = (ImageView) findViewById(R.id.button_decrease);
mPlusButton.setImageResource(rightIcon);
}
tintButton(mPlusButton, mButtonColorStateList);
setButtonLayoutParams(mPlusButton);
}
private void setButtonMinusImage() {
if (mOrientation == LinearLayout.VERTICAL) {
mMinusButton = (ImageView) findViewById(R.id.button_decrease);
mMinusButton.setImageResource(downIcon);
} else if (mOrientation == LinearLayout.HORIZONTAL) {
mMinusButton = (ImageView) findViewById(R.id.button_increase);
mMinusButton.setImageResource(leftIcon);
}
tintButton(mMinusButton, mButtonColorStateList);
setButtonLayoutParams(mMinusButton);
}
private void setButtonLayoutParams(ImageView button) {
LayoutParams params = (LayoutParams) button.getLayoutParams();
params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
params.setMargins(0, 0, 0, 0);
button.setLayoutParams(params);
button.setPadding(mButtonPaddingLeft, mButtonPaddingTop, mButtonPaddingRight, mButtonPaddingBottom);
}
private void tintButton(@NonNull ImageView button, ColorStateList colorStateList) {
Drawable drawable = DrawableCompat.wrap(button.getDrawable());
DrawableCompat.setTintList(drawable, colorStateList);
button.setImageDrawable(drawable);
}
private void scaleImageViewDrawable(ImageView view, float scaleFactor) {
Drawable drawable = view.getDrawable();
int currentWidth = drawable.getIntrinsicWidth();
int currentHeight = drawable.getIntrinsicHeight();
int newWidth = (int) (currentWidth * scaleFactor);
int newHeight = (int) (currentHeight * scaleFactor);
if (newWidth < currentWidth && newHeight < currentHeight) {
int marginWidth = (currentWidth - newWidth) / 2;
int marginHeight = (currentHeight - newHeight) / 2;
//setBounds is not working on FireTV, that's why we use a workaround with margins
LayoutParams params = (LayoutParams) view.getLayoutParams();
params.width = newWidth;
params.height = newHeight;
params.setMargins(marginWidth, marginHeight, marginWidth, marginHeight);
view.setLayoutParams(params);
}
}
private void increment() {
if (mValue < mMaxValue) {
setValue(mValue + mStepSize);
}
}
private void decrement() {
if (mValue > mMinValue) {
setValue(mValue - mStepSize);
}
}
private class RepeatRunnable implements Runnable {
public void run() {
if (mAutoIncrement) {
increment();
mUpdateIntervalHandler.postDelayed(new RepeatRunnable(), mUpdateIntervalMillis);
} else if (mAutoDecrement) {
decrement();
mUpdateIntervalHandler.postDelayed(new RepeatRunnable(), mUpdateIntervalMillis);
}
}
}
private class RepeatSlowingRunnable implements Runnable {
long mUpdateIntervalMillis = 0;
int mNumberOfLeftRuns = 0;
RepeatSlowingRunnable(int numberOfLeftRuns, long millis) {
mUpdateIntervalMillis = millis;
mNumberOfLeftRuns = numberOfLeftRuns;
}
public void run() {
long millisNextRun = (long) (mUpdateIntervalMillis * SLOWING_FACTOR);
if (mNumberOfLeftRuns > 0) {
int leftRuns = mNumberOfLeftRuns - 1;
if (mAutoIncrement) {
increment();
mUpdateIntervalHandler.postDelayed(new RepeatSlowingRunnable(leftRuns, millisNextRun), mUpdateIntervalMillis);
} else if (mAutoDecrement) {
decrement();
mUpdateIntervalHandler.postDelayed(new RepeatSlowingRunnable(leftRuns, millisNextRun), mUpdateIntervalMillis);
}
} else {
mAutoIncrement = false;
mAutoDecrement = false;
}
}
}
}