From 0ea34ee9f4410bb6e00546186ac3a00942b4f680 Mon Sep 17 00:00:00 2001 From: Michael Muenzer Date: Tue, 2 May 2017 22:31:21 +0200 Subject: [PATCH] Added possibility to change numbers via scrolling --- .gitignore | 2 +- README.md | 16 +- .../ScrollableNumberPicker.java | 442 ++++++++++++------ library/src/main/res/values/attrs.xml | 1 + library/src/main/res/values/boolean.xml | 4 + library/src/main/res/values/dimen.xml | 2 + .../src/main/res/layout/activity_main.xml | 40 ++ 7 files changed, 354 insertions(+), 153 deletions(-) create mode 100644 library/src/main/res/values/boolean.xml diff --git a/.gitignore b/.gitignore index ecf627d..e5e5f47 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .gradle /local.properties /.idea -/build +/build/* diff --git a/README.md b/README.md index c321c3b..ee1a5c5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ScrollableNumberPicker ============ This view provides an user-friendly numerical input interface. It can easily be customized and is built to be used on Android-TV as well. +[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-ScrollableNumberPicker-orange.svg?style=flat)](https://android-arsenal.com/details/1/5676) [![Download](https://api.bintray.com/packages/michaelmuenzer/ScrollableNumberPicker/ScrollableNumberPicker/images/download.svg) ](https://bintray.com/michaelmuenzer/ScrollableNumberPicker/ScrollableNumberPicker/_latestVersion) [![API](https://img.shields.io/badge/API-16%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=16) @@ -32,7 +33,16 @@ Just include `ScrollableNumberPicker` inside our xml-layout. There are samples a android:layout_height="wrap_content"/> ``` -You can make use of various custom attributes to define how the increment and decrement interactions should behave. +If you want to change the value by scrolling you can enable and control it like this: +```xml + +``` + +You can make use of various other custom attributes to define how the increment and decrement interactions should behave: ```xml ``` -There exist attributes which let you customize the general appearance of the view. +There exist further attributes which let you customize the general appearance of the view: ```xml ``` -You can essentially make the element look exactly like you want by using the `android:background` attribute +You can essentially make the element look exactly like you want by using the `android:background` attribute. ```xml 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); + } + } + + @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; @@ -114,6 +305,9 @@ private void init(Context context, AttributeSet attrs) { mValue = typedArray.getInt(R.styleable.ScrollableNumberPicker_snp_value, res.getInteger(R.integer.default_value)); + 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)); @@ -150,6 +344,80 @@ private void initViews() { initButtonPlus(); initButtonMinus(); + + 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 initButtonPlus() { @@ -282,21 +550,6 @@ private void scaleImageViewDrawable(ImageView view, float scaleFactor) { } } - @SuppressWarnings("unused") - public ImageView getButtonMinusView() { - return mMinusButton; - } - - @SuppressWarnings("unused") - public ImageView getButtonPlusView() { - return mPlusButton; - } - - @SuppressWarnings("unused") - public TextView getTextValueView() { - return mValueTextView; - } - private void increment() { if (mValue < mMaxValue) { setValue(mValue + mStepSize); @@ -309,152 +562,43 @@ private void decrement() { } } - @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); - } - } - - @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(); + 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); + } } } - @SuppressWarnings("unused") - public int getStepSize() { - return mStepSize; - } - - @SuppressWarnings("unused") - public void setStepSize(int stepSize) { - mStepSize = stepSize; - } - - @SuppressWarnings("unused") - public long getOnLongPressUpdateInterval() { - return mUpdateIntervalMillis; - } + private class RepeatSlowingRunnable implements Runnable { + long mUpdateIntervalMillis = 0; + int mNumberOfLeftRuns = 0; - @SuppressWarnings("unused") - public void setOnLongPressUpdateInterval(int intervalMillis) { - if (intervalMillis < MIN_UPDATE_INTERVAL_MS) { - intervalMillis = MIN_UPDATE_INTERVAL_MS; + RepeatSlowingRunnable(int numberOfLeftRuns, long millis) { + mUpdateIntervalMillis = millis; + mNumberOfLeftRuns = numberOfLeftRuns; } - mUpdateIntervalMillis = intervalMillis; - } + public void run() { + long millisNextRun = (long) (mUpdateIntervalMillis * SLOWING_FACTOR); - @SuppressWarnings("unused") - public void setListener(ScrollableNumberPickerListener listener) { - mListener = listener; - } + if (mNumberOfLeftRuns > 0) { + int leftRuns = mNumberOfLeftRuns - 1; - 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); - } + if (mAutoIncrement) { 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); - } + mUpdateIntervalHandler.postDelayed(new RepeatSlowingRunnable(leftRuns, millisNextRun), mUpdateIntervalMillis); + } else if (mAutoDecrement) { 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; + mUpdateIntervalHandler.postDelayed(new RepeatSlowingRunnable(leftRuns, millisNextRun), mUpdateIntervalMillis); } } else { - if (keyCode == KEYCODE_DPAD_UP) { - setButtonPlusImage(); - return true; - } else if (keyCode == KEYCODE_DPAD_DOWN) { - setButtonMinusImage(); - return true; - } - } - } - - return false; - } - - 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); + mAutoIncrement = false; + mAutoDecrement = false; } } } diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index e2263f9..4e81f8f 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -13,6 +13,7 @@ + diff --git a/library/src/main/res/values/boolean.xml b/library/src/main/res/values/boolean.xml new file mode 100644 index 0000000..9d18ed5 --- /dev/null +++ b/library/src/main/res/values/boolean.xml @@ -0,0 +1,4 @@ + + + false + \ No newline at end of file diff --git a/library/src/main/res/values/dimen.xml b/library/src/main/res/values/dimen.xml index eaf591a..76106ab 100644 --- a/library/src/main/res/values/dimen.xml +++ b/library/src/main/res/values/dimen.xml @@ -3,6 +3,8 @@ 24dp 0dp 0dp + 4dp + 25dp 1.0 \ No newline at end of file diff --git a/sample-mobile/src/main/res/layout/activity_main.xml b/sample-mobile/src/main/res/layout/activity_main.xml index 3da1c6c..c922d7a 100644 --- a/sample-mobile/src/main/res/layout/activity_main.xml +++ b/sample-mobile/src/main/res/layout/activity_main.xml @@ -83,6 +83,46 @@ app:snp_value="10" app:snp_valueMarginEnd="5dp" app:snp_valueMarginStart="5dp"/> + + + + + +