diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d191a0c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.gradle
+/local.properties
+local.properties
+bintray.properties
+/.idea
+.DS_Store
+/build
+*.iml
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..34ee0f7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,91 @@
+# DiscreteSeekBar
+
+This is a copy of [DiscreteSeekBar](https://github.com/AnderWeb/discreteSeekBar) by [Gustavo Claramunt](https://github.com/AnderWeb)
+
+## Description
+DiscreteSeekbar is my poor attempt to develop an android implementation of the [Discrete Slider] component from the Google Material Design Guidelines.
+
+## Prologe
+I really hope Google provides developers with a better (and official) implementation ;)
+
+## Warning
+After a bunch of hours trying to replicate the exact feel of the Material's Discrete Seekbar, with a beautiful stuff-that-morphs-into-other-stuff animation I was convinced about releasing the current code.
+
+```java
+android.util.Log.wtf("WARNING!! HACKERY-DRAGONS!!");
+```
+I've done a few bit of hacky cede and a bunch of things I'm not completely proud of, so use under your sole responsibility (or help me improve it via pull-requests!)
+
+## Implementation details
+This thing runs on minSDK=7 (well, technically could run 4 but can't test since AVDs for api 4 are deprecated and just don't boot).
+Obviously some of the subtle animations (navigating with the Keyboard, the Ripple effect, text fade ins/fade outs, etc) are not going to work on APIS lower than 11, but the bubble thing does. And I haven't found a way of improving this with 11-21 APIs, so...
+
+The base SeekBar is pretty simple. Just 3 drawables for the track, progress and thumb. Some touch event logic to drag, some key event logic to move, and that's all.
+
+It supports custom ranges (custom min/max), even for negative values.
+
+The bubble thing **DOESN'T USE** [VectorDrawableMagic] . I was not really needed for such a simple morph. It uses instead an [Animatable Drawable] for the animation with a lot of hackery for callbacks, drawing and a bunch of old simple math.
+
+>For this to work (and sync with events, etc) I've written a fair amount of shit questionable code...
+
+The material-floating-thing is composed into the WindowManager (like the typical overflow menus) to be able to show it over other Views without needing to set the SeekBar big enough to account for the (variable) size of he floating thing.
+
+>For this I'm not sure about the amounts of things I've copied from [PopupWindow] and the possible issues.
+
+## Usage
+This is published in my JFrog repo:
+
+```groovy
+repositories {
+ maven {
+ url "https://dl.bintray.com/mardous/Maven"
+ }
+}
+
+dependencies {
+ implementation 'com.mardous:discrete-seekbar:1.0.0'
+}
+```
+
+Once imported into your project, you just need to put them into your layous like:
+
+```xml
+
+ * Also if the current progress is out of the new range, it will be set to MIN + *
+ * + * @param max + * @see #setMin(int) + * @see #setProgress(int) + */ + public void setMax(int max) { + mMax = max; + if (mMax < mMin) { + setMin(mMax - 1); + } + updateKeyboardRange(); + + if (mValue < mMin || mValue > mMax) { + setProgress(mMin); + } + //We need to refresh the PopupIndicator view + updateIndicatorSizes(); + } + + public int getMax() { + return mMax; + } + + /** + * Sets the minimum value for this DiscreteSeekBar + * if the supplied argument is bigger than the Current MAX value, + * the MAX value will be set to MIN+1 + *+ * Also if the current progress is out of the new range, it will be set to MIN + *
+ * + * @param min the value. + * @see #setMax(int) + * @see #setProgress(int) + */ + public void setMin(int min) { + mMin = min; + if (mMin > mMax) { + setMax(mMin + 1); + } + updateKeyboardRange(); + + if (mValue < mMin || mValue > mMax) { + setProgress(mMin); + } + } + + public int getMin() { + return mMin; + } + + /** + * Sets the current progress for this DiscreteSeekBar + * The supplied argument will be capped to the current MIN-MAX range + * + * @param progress the progress. + * @see #setMax(int) + * @see #setMin(int) + */ + public void setProgress(int progress) { + setProgress(progress, false); + } + + private void setProgress(int value, boolean fromUser) { + value = Math.max(mMin, Math.min(mMax, value)); + if (isAnimationRunning()) { + mPositionAnimator.cancel(); + } + + if (mValue != value) { + mValue = value; + notifyProgress(value, fromUser); + updateProgressMessage(value); + updateThumbPosFromCurrentProgress(); + } + } + + /** + * Get the current progress + * + * @return the current progress :-P + */ + public int getProgress() { + return mValue; + } + + /** + * Sets a listener to receive notifications of changes to the DiscreteSeekBar's progress level. Also + * provides notifications of when the DiscreteSeekBar shows/hides the bubble indicator. + * + * @param listener The seek bar notification listener + * @see DiscreteSeekBar.OnProgressChangeListener + */ + public void setOnProgressChangeListener(@Nullable OnProgressChangeListener listener) { + mPublicChangeListener = listener; + } + + /** + * Sets the color of the seek thumb, as well as the color of the popup indicator. + * + * @param thumbColor The color the seek thumb will be changed to + * @param indicatorColor The color the popup indicator will be changed to + * The indicator will animate from thumbColor to indicatorColor + * when opening + */ + public void setThumbColor(int thumbColor, int indicatorColor) { + mThumb.setColorStateList(ColorStateList.valueOf(thumbColor)); + mIndicator.setColors(indicatorColor, thumbColor); + } + + /** + * Sets the color of the seek thumb, as well as the color of the popup indicator. + * + * @param thumbColorStateList The ColorStateList the seek thumb will be changed to + * @param indicatorColor The color the popup indicator will be changed to + * The indicator will animate from thumbColorStateList(pressed state) to indicatorColor + * when opening + */ + public void setThumbColor(@NonNull ColorStateList thumbColorStateList, int indicatorColor) { + mThumb.setColorStateList(thumbColorStateList); + //we use the "pressed" color to morph the indicator from it to its own color + int thumbColor = thumbColorStateList.getColorForState(new int[]{PRESSED_STATE}, thumbColorStateList.getDefaultColor()); + mIndicator.setColors(indicatorColor, thumbColor); + } + + /** + * Sets the color of the seekbar scrubber + * + * @param color The color the track scrubber will be changed to + */ + public void setScrubberColor(int color) { + mScrubber.setColorStateList(ColorStateList.valueOf(color)); + } + + /** + * Sets the color of the seekbar scrubber + * + * @param colorStateList The ColorStateList the track scrubber will be changed to + */ + public void setScrubberColor(@NonNull ColorStateList colorStateList) { + mScrubber.setColorStateList(colorStateList); + } + + /** + * Sets the color of the seekbar ripple + * + * @param color The color the track ripple will be changed to + */ + public void setRippleColor(int color) { + setRippleColor(new ColorStateList(new int[][]{new int[]{}}, new int[]{color})); + } + + /** + * Sets the color of the seekbar ripple + * + * @param colorStateList The ColorStateList the track ripple will be changed to + */ + public void setRippleColor(@NonNull ColorStateList colorStateList) { + SeekBarCompat.setRippleColor(mRipple, colorStateList); + } + + /** + * Sets the color of the seekbar scrubber + * + * @param color The color the track will be changed to + */ + public void setTrackColor(int color) { + mTrack.setColorStateList(ColorStateList.valueOf(color)); + } + + /** + * Sets the color of the seekbar scrubber + * + * @param colorStateList The ColorStateList the track will be changed to + */ + public void setTrackColor(@NonNull ColorStateList colorStateList) { + mTrack.setColorStateList(colorStateList); + } + + /** + * If {@code enabled} is false the indicator won't appear. By default popup indicator is + * enabled. + */ + public void setIndicatorPopupEnabled(boolean enabled) { + this.mIndicatorPopupEnabled = enabled; + } + + private void updateIndicatorSizes() { + if (!isInEditMode()) { + if (mNumericTransformer.useStringTransform()) { + mIndicator.updateSizes(mNumericTransformer.transformToString(mMax)); + } else { + mIndicator.updateSizes(convertValueToMessage(mNumericTransformer.transform(mMax))); + } + } + + } + + private void notifyProgress(int value, boolean fromUser) { + if (mPublicChangeListener != null) { + mPublicChangeListener.onProgressChanged(DiscreteSeekBar.this, value, fromUser); + } + onValueChanged(value); + } + + private void notifyBubble(boolean open) { + if (open) { + onShowBubble(); + } else { + onHideBubble(); + } + } + + /** + * When the {@link DiscreteSeekBar} enters pressed or focused state + * the bubble with the value will be shown, and this method called + *+ * Subclasses may override this to add functionality around this event + *
+ */ + protected void onShowBubble() { + } + + /** + * When the {@link DiscreteSeekBar} exits pressed or focused state + * the bubble with the value will be hidden, and this method called + *+ * Subclasses may override this to add functionality around this event + *
+ */ + protected void onHideBubble() { + } + + /** + * When the {@link DiscreteSeekBar} value changes this method is called + *+ * Subclasses may override this to add functionality around this event + * without having to specify a {@link DiscreteSeekBar.OnProgressChangeListener} + *
+ */ + protected void onValueChanged(int value) { + } + + private void updateKeyboardRange() { + int range = mMax - mMin; + if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) { + // It will take the user too long to change this via keys, change it + // to something more reasonable + mKeyProgressIncrement = Math.max(1, Math.round((float) range / 20)); + } + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int height = mThumb.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom(); + height += (mAddedTouchBounds * 2); + setMeasuredDimension(widthSize, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) { + removeCallbacks(mShowIndicatorRunnable); + if (!isInEditMode()) { + mIndicator.dismissComplete(); + } + updateFromDrawableState(); + } + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + super.scheduleDrawable(who, what, when); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + int thumbWidth = mThumb.getIntrinsicWidth(); + int thumbHeight = mThumb.getIntrinsicHeight(); + int addedThumb = mAddedTouchBounds; + int halfThumb = thumbWidth / 2; + int paddingLeft = getPaddingLeft() + addedThumb; + int paddingRight = getPaddingRight(); + int bottom = getHeight() - getPaddingBottom() - addedThumb; + mThumb.setBounds(paddingLeft, bottom - thumbHeight, paddingLeft + thumbWidth, bottom); + int trackHeight = Math.max(mTrackHeight / 2, 1); + mTrack.setBounds(paddingLeft + halfThumb, bottom - halfThumb - trackHeight, + getWidth() - halfThumb - paddingRight - addedThumb, bottom - halfThumb + trackHeight); + int scrubberHeight = Math.max(mScrubberHeight / 2, 2); + mScrubber.setBounds(paddingLeft + halfThumb, bottom - halfThumb - scrubberHeight, + paddingLeft + halfThumb, bottom - halfThumb + scrubberHeight); + + //Update the thumb position after size changed + updateThumbPosFromCurrentProgress(); + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + if (!isLollipopOrGreater) { + mRipple.draw(canvas); + } + super.onDraw(canvas); + mTrack.draw(canvas); + mScrubber.draw(canvas); + mThumb.draw(canvas); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + updateFromDrawableState(); + } + + private void updateFromDrawableState() { + int[] state = getDrawableState(); + boolean focused = false; + boolean pressed = false; + for (int i : state) { + if (i == FOCUSED_STATE) { + focused = true; + } else if (i == PRESSED_STATE) { + pressed = true; + } + } + if (isEnabled() && (focused || pressed) && mIndicatorPopupEnabled) { + //We want to add a small delay here to avoid + //poping in/out on simple taps + removeCallbacks(mShowIndicatorRunnable); + postDelayed(mShowIndicatorRunnable, INDICATOR_DELAY_FOR_TAPS); + } else { + hideFloater(); + } + mThumb.setState(state); + mTrack.setState(state); + mScrubber.setState(state); + mRipple.setState(state); + } + + private void updateProgressMessage(int value) { + if (!isInEditMode()) { + if (mNumericTransformer.useStringTransform()) { + mIndicator.setValue(mNumericTransformer.transformToString(value)); + } else { + mIndicator.setValue(convertValueToMessage(mNumericTransformer.transform(value))); + } + } + } + + private String convertValueToMessage(int value) { + String format = mIndicatorFormatter != null ? mIndicatorFormatter : DEFAULT_FORMATTER; + //We're trying to re-use the Formatter here to avoid too much memory allocations + //But I'm not completey sure if it's doing anything good... :( + //Previously, this condition was wrong so the Formatter was always re-created + //But as I fixed the condition, the formatter started outputting trash characters from previous + //calls, so I mark the StringBuilder as empty before calling format again. + + //Anyways, I see the memory usage still go up on every call to this method + //and I have no clue on how to fix that... damn Strings... + if (mFormatter == null || !mFormatter.locale().equals(Locale.getDefault())) { + int bufferSize = format.length() + String.valueOf(mMax).length(); + if (mFormatBuilder == null) { + mFormatBuilder = new StringBuilder(bufferSize); + } else { + mFormatBuilder.ensureCapacity(bufferSize); + } + mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); + } else { + mFormatBuilder.setLength(0); + } + return mFormatter.format(format, value).toString(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) { + return false; + } + int actionMasked = event.getActionMasked(); + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: + mDownX = event.getX(); + startDragging(event, isInScrollingContainer()); + break; + case MotionEvent.ACTION_MOVE: + if (isDragging()) { + updateDragging(event); + } else { + final float x = event.getX(); + if (Math.abs(x - mDownX) > mTouchSlop) { + startDragging(event, false); + } + } + break; + case MotionEvent.ACTION_UP: + if (!isDragging() && mAllowTrackClick) { + startDragging(event, false); + updateDragging(event); + } + stopDragging(); + break; + case MotionEvent.ACTION_CANCEL: + stopDragging(); + break; + } + return true; + } + + private boolean isInScrollingContainer() { + ViewParent p = getParent(); + while (p instanceof ViewGroup) { + if (((ViewGroup) p).shouldDelayChildPressedState()) { + return true; + } + p = p.getParent(); + } + return false; + } + + private boolean startDragging(MotionEvent ev, boolean ignoreTrackIfInScrollContainer) { + final Rect bounds = mTempRect; + mThumb.copyBounds(bounds); + //Grow the current thumb rect for a bigger touch area + bounds.inset(-mAddedTouchBounds, -mAddedTouchBounds); + mIsDragging = (bounds.contains((int) ev.getX(), (int) ev.getY())); + if (!mIsDragging && mAllowTrackClick && !ignoreTrackIfInScrollContainer) { + //If the user clicked outside the thumb, we compute the current position + //and force an immediate drag to it. + mIsDragging = true; + mDragOffset = (bounds.width() / 2) - mAddedTouchBounds; + updateDragging(ev); + //As the thumb may have moved, get the bounds again + mThumb.copyBounds(bounds); + bounds.inset(-mAddedTouchBounds, -mAddedTouchBounds); + } + if (mIsDragging) { + setPressed(true); + attemptClaimDrag(); + setHotspot(ev.getX(), ev.getY()); + mDragOffset = (int) (ev.getX() - bounds.left - mAddedTouchBounds); + if (mPublicChangeListener != null) { + mPublicChangeListener.onStartTrackingTouch(this); + } + } + return mIsDragging; + } + + private boolean isDragging() { + return mIsDragging; + } + + private void stopDragging() { + if (mPublicChangeListener != null) { + mPublicChangeListener.onStopTrackingTouch(this); + } + mIsDragging = false; + setPressed(false); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + //TODO: Should we reverse the keys for RTL? The framework's SeekBar does NOT.... + boolean handled = false; + if (isEnabled()) { + int progress = getAnimatedProgress(); + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = true; + if (progress <= mMin) break; + animateSetProgress(progress - mKeyProgressIncrement); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = true; + if (progress >= mMax) break; + animateSetProgress(progress + mKeyProgressIncrement); + break; + } + } + + return handled || super.onKeyDown(keyCode, event); + } + + private int getAnimatedProgress() { + return isAnimationRunning() ? getAnimationTarget() : mValue; + } + + + private boolean isAnimationRunning() { + return mPositionAnimator != null && mPositionAnimator.isRunning(); + } + + private void animateSetProgress(int progress) { + final float curProgress = isAnimationRunning() ? getAnimationPosition() : getProgress(); + + if (progress < mMin) { + progress = mMin; + } else if (progress > mMax) { + progress = mMax; + } + //setProgressValueOnly(progress); + + if (mPositionAnimator != null) { + mPositionAnimator.cancel(); + } + + mAnimationTarget = progress; + mPositionAnimator = ValueAnimator.ofFloat(curProgress, progress); + mPositionAnimator.setDuration(PROGRESS_ANIMATION_DURATION); + mPositionAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + setAnimationPosition((float) valueAnimator.getAnimatedValue()); + } + }); + + mPositionAnimator.start(); + } + + private int getAnimationTarget() { + return mAnimationTarget; + } + + private void setAnimationPosition(float position) { + mAnimationPosition = position; + float currentScale = (position - mMin) / (float) (mMax - mMin); + updateProgressFromAnimation(currentScale); + } + + private float getAnimationPosition() { + return mAnimationPosition; + } + + private void updateDragging(MotionEvent ev) { + setHotspot(ev.getX(), ev.getY()); + int x = (int) ev.getX(); + Rect oldBounds = mThumb.getBounds(); + int halfThumb = oldBounds.width() / 2; + int addedThumb = mAddedTouchBounds; + int newX = x - mDragOffset + halfThumb; + int left = getPaddingLeft() + halfThumb + addedThumb; + int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb); + if (newX < left) { + newX = left; + } else if (newX > right) { + newX = right; + } + + int available = right - left; + float scale = (float) (newX - left) / (float) available; + if (isRtl()) { + scale = 1f - scale; + } + int progress = Math.round((scale * (mMax - mMin)) + mMin); + setProgress(progress, true); + } + + private void updateProgressFromAnimation(float scale) { + Rect bounds = mThumb.getBounds(); + int halfThumb = bounds.width() / 2; + int addedThumb = mAddedTouchBounds; + int left = getPaddingLeft() + halfThumb + addedThumb; + int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb); + int available = right - left; + int progress = Math.round((scale * (mMax - mMin)) + mMin); + //we don't want to just call setProgress here to avoid the animation being cancelled, + //and this position is not bound to a real progress value but interpolated + if (progress != getProgress()) { + mValue = progress; + notifyProgress(mValue, true); + updateProgressMessage(progress); + } + final int thumbPos = (int) (scale * available + 0.5f); + updateThumbPos(thumbPos); + } + + private void updateThumbPosFromCurrentProgress() { + int thumbWidth = mThumb.getIntrinsicWidth(); + int addedThumb = mAddedTouchBounds; + int halfThumb = thumbWidth / 2; + float scaleDraw = (mValue - mMin) / (float) (mMax - mMin); + + //This doesn't matter if RTL, as we just need the "avaiable" area + int left = getPaddingLeft() + halfThumb + addedThumb; + int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb); + int available = right - left; + + final int thumbPos = (int) (scaleDraw * available + 0.5f); + updateThumbPos(thumbPos); + } + + private void updateThumbPos(int posX) { + int thumbWidth = mThumb.getIntrinsicWidth(); + int halfThumb = thumbWidth / 2; + int start; + if (isRtl()) { + start = getWidth() - getPaddingRight() - mAddedTouchBounds; + posX = start - posX - thumbWidth; + } else { + start = getPaddingLeft() + mAddedTouchBounds; + posX = start + posX; + } + mThumb.copyBounds(mInvalidateRect); + mThumb.setBounds(posX, mInvalidateRect.top, posX + thumbWidth, mInvalidateRect.bottom); + if (isRtl()) { + mScrubber.getBounds().right = start - halfThumb; + mScrubber.getBounds().left = posX + halfThumb; + } else { + mScrubber.getBounds().left = start + halfThumb; + mScrubber.getBounds().right = posX + halfThumb; + } + final Rect finalBounds = mTempRect; + mThumb.copyBounds(finalBounds); + if (!isInEditMode()) { + mIndicator.move(finalBounds.centerX()); + } + + mInvalidateRect.inset(-mAddedTouchBounds, -mAddedTouchBounds); + finalBounds.inset(-mAddedTouchBounds, -mAddedTouchBounds); + mInvalidateRect.union(finalBounds); + SeekBarCompat.setHotspotBounds(mRipple, finalBounds.left, finalBounds.top, finalBounds.right, finalBounds.bottom); + invalidate(mInvalidateRect); + } + + + private void setHotspot(float x, float y) { + DrawableCompat.setHotspot(mRipple, x, y); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mThumb || who == mTrack || who == mScrubber || who == mRipple || super.verifyDrawable(who); + } + + private void attemptClaimDrag() { + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + private Runnable mShowIndicatorRunnable = new Runnable() { + @Override + public void run() { + showFloater(); + } + }; + + private void showFloater() { + if (!isInEditMode()) { + mThumb.animateToPressed(); + mIndicator.showIndicator(this, mThumb.getBounds()); + notifyBubble(true); + } + } + + private void hideFloater() { + removeCallbacks(mShowIndicatorRunnable); + if (!isInEditMode()) { + mIndicator.dismiss(); + notifyBubble(false); + } + } + + private MarkerDrawable.MarkerAnimationListener mFloaterListener = new MarkerDrawable.MarkerAnimationListener() { + @Override + public void onClosingComplete() { + mThumb.animateToNormal(); + } + + @Override + public void onOpeningComplete() { + } + }; + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeCallbacks(mShowIndicatorRunnable); + if (!isInEditMode()) { + mIndicator.dismissComplete(); + } + } + + public boolean isRtl() { + return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) && mMirrorForRtl; + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + CustomState state = new CustomState(superState); + state.progress = getProgress(); + state.max = mMax; + state.min = mMin; + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(CustomState.class)) { + super.onRestoreInstanceState(state); + return; + } + + CustomState customState = (CustomState) state; + setMin(customState.min); + setMax(customState.max); + setProgress(customState.progress, false); + super.onRestoreInstanceState(customState.getSuperState()); + } + + static class CustomState extends BaseSavedState { + private int progress; + private int max; + private int min; + + public CustomState(Parcel source) { + super(source); + progress = source.readInt(); + max = source.readInt(); + min = source.readInt(); + } + + public CustomState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel outcoming, int flags) { + super.writeToParcel(outcoming, flags); + outcoming.writeInt(progress); + outcoming.writeInt(max); + outcoming.writeInt(min); + } + + public static final Creator+ * I've used this to be able to accommodate the TextView + * and the {@link com.mardous.discreteseekbar.internal.drawable.MarkerDrawable} + * with the required positions and offsets + *
+ * + * @hide + */ +public class Marker extends ViewGroup implements MarkerDrawable.MarkerAnimationListener { + private static final int PADDING_DP = 4; + private static final int ELEVATION_DP = 8; + //The TextView to show the info + private TextView mNumber; + //The max width of this View + private int mWidth; + //some distance between the thumb and our bubble marker. + //This will be added to our measured height + private int mSeparation; + MarkerDrawable mMarkerDrawable; + + public Marker(Context context, AttributeSet attrs, int defStyleAttr, String maxValue, int thumbSize, int separation) { + super(context, attrs, defStyleAttr); + //as we're reading the parent DiscreteSeekBar attributes, it may wrongly set this view's visibility. + setVisibility(View.VISIBLE); + + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSeekBar, + R.attr.discreteSeekBarStyle, R.style.Widget_DiscreteSeekBar); + + int padding = (int) (PADDING_DP * displayMetrics.density) * 2; + int textAppearanceId = a.getResourceId(R.styleable.DiscreteSeekBar_dsb_indicatorTextAppearance, + R.style.Widget_DiscreteIndicatorTextAppearance); + mNumber = new TextView(context); + //Add some padding to this textView so the bubble has some space to breath + mNumber.setPadding(padding, 0, padding, 0); + mNumber.setTextAppearance(context, textAppearanceId); + mNumber.setGravity(Gravity.CENTER); + mNumber.setText(maxValue); + mNumber.setMaxLines(1); + mNumber.setSingleLine(true); + SeekBarCompat.setTextDirection(mNumber, TEXT_DIRECTION_LOCALE); + mNumber.setVisibility(View.INVISIBLE); + + //add some padding for the elevation shadow not to be clipped + //I'm sure there are better ways of doing this... + setPadding(padding, padding, padding, padding); + + resetSizes(maxValue); + + mSeparation = separation; + ColorStateList color = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_indicatorColor); + mMarkerDrawable = new MarkerDrawable(color, thumbSize); + mMarkerDrawable.setCallback(this); + mMarkerDrawable.setMarkerListener(this); + mMarkerDrawable.setExternalOffset(padding); + + //Elevation for anroid 5+ + float elevation = a.getDimension(R.styleable.DiscreteSeekBar_dsb_indicatorElevation, ELEVATION_DP * displayMetrics.density); + ViewCompat.setElevation(this, elevation); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + SeekBarCompat.setOutlineProvider(this, mMarkerDrawable); + } + a.recycle(); + } + + public void resetSizes(String maxValue) { + DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + //Account for negative numbers... is there any proper way of getting the biggest string between our range???? + mNumber.setText("-" + maxValue); + //Do a first forced measure call for the TextView (with the biggest text content), + //to calculate the max width and use always the same. + //this avoids the TextView from shrinking and growing when the text content changes + int wSpec = MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, MeasureSpec.AT_MOST); + int hSpec = MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, MeasureSpec.AT_MOST); + mNumber.measure(wSpec, hSpec); + mWidth = Math.max(mNumber.getMeasuredWidth(), mNumber.getMeasuredHeight()); + removeView(mNumber); + addView(mNumber, new FrameLayout.LayoutParams(mWidth, mWidth, Gravity.LEFT | Gravity.TOP)); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + mMarkerDrawable.draw(canvas); + super.dispatchDraw(canvas); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + measureChildren(widthMeasureSpec, heightMeasureSpec); + int widthSize = mWidth + getPaddingLeft() + getPaddingRight(); + int heightSize = mWidth + getPaddingTop() + getPaddingBottom(); + //This diff is the basic calculation of the difference between + //a square side size and its diagonal + //this helps us account for the visual offset created by MarkerDrawable + //when leaving one of the corners un-rounded + int diff = (int) ((1.41f * mWidth) - mWidth) / 2; + setMeasuredDimension(widthSize, heightSize + diff + mSeparation); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = getWidth() - getPaddingRight(); + int bottom = getHeight() - getPaddingBottom(); + //the TetView is always layout at the top + mNumber.layout(left, top, left + mWidth, top + mWidth); + //the MarkerDrawable uses the whole view, it will adapt itself... + // or it seems so... + mMarkerDrawable.setBounds(left, top, right, bottom); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mMarkerDrawable || super.verifyDrawable(who); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + //HACK: Sometimes, the animateOpen() call is made before the View is attached + //so the drawable cannot schedule itself to run the animation + //I think we can call it here safely. + //I've seen it happen in android 2.3.7 + animateOpen(); + } + + public void setValue(CharSequence value) { + mNumber.setText(value); + } + + public CharSequence getValue() { + return mNumber.getText(); + } + + public void animateOpen() { + mMarkerDrawable.stop(); + mMarkerDrawable.animateToPressed(); + } + + public void animateClose() { + mMarkerDrawable.stop(); + mNumber.setVisibility(View.INVISIBLE); + mMarkerDrawable.animateToNormal(); + } + + @Override + public void onOpeningComplete() { + mNumber.setVisibility(View.VISIBLE); + if (getParent() instanceof MarkerDrawable.MarkerAnimationListener) { + ((MarkerDrawable.MarkerAnimationListener) getParent()).onOpeningComplete(); + } + } + + @Override + public void onClosingComplete() { + if (getParent() instanceof MarkerDrawable.MarkerAnimationListener) { + ((MarkerDrawable.MarkerAnimationListener) getParent()).onClosingComplete(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mMarkerDrawable.stop(); + } + + public void setColors(int startColor, int endColor) { + mMarkerDrawable.setColors(startColor, endColor); + } +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/PopupIndicator.java b/library/src/main/java/com/mardous/discreteseekbar/internal/PopupIndicator.java new file mode 100644 index 0000000..1719ae1 --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/PopupIndicator.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import androidx.core.view.GravityCompat; +import com.mardous.discreteseekbar.internal.drawable.MarkerDrawable; + +/** + * Class to manage the floating bubble thing, similar (but quite worse tested than {@link android.widget.PopupWindow} + * + *+ * This will attach a View to the Window (full-width, measured-height, positioned just under the thumb) + *
+ * + * @hide + * @see #showIndicator(android.view.View, android.graphics.Rect) + * @see #dismiss() + * @see #dismissComplete() + * @see com.mardous.discreteseekbar.internal.PopupIndicator.Floater + */ +public class PopupIndicator { + + private final WindowManager mWindowManager; + private boolean mShowing; + private Floater mPopupView; + //Outside listener for the DiscreteSeekBar to get MarkerDrawable animation events. + //The whole chain of events goes this way: + //MarkerDrawable->Marker->Floater->mListener->DiscreteSeekBar.... + //... phew! + private MarkerDrawable.MarkerAnimationListener mListener; + private int[] mDrawingLocation = new int[2]; + Point screenSize = new Point(); + + public PopupIndicator(Context context, AttributeSet attrs, int defStyleAttr, String maxValue, int thumbSize, int separation) { + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mPopupView = new Floater(context, attrs, defStyleAttr, maxValue, thumbSize, separation); + } + + public void updateSizes(String maxValue) { + dismissComplete(); + if (mPopupView != null) { + mPopupView.mMarker.resetSizes(maxValue); + } + } + + public void setListener(MarkerDrawable.MarkerAnimationListener listener) { + mListener = listener; + } + + /** + * We want the Floater to be full-width because the contents will be moved from side to side. + * We may/should change this in the future to use just the PARENT View width and/or pass it in the constructor + */ + private void measureFloater() { + int specWidth = View.MeasureSpec.makeMeasureSpec(screenSize.x, View.MeasureSpec.EXACTLY); + int specHeight = View.MeasureSpec.makeMeasureSpec(screenSize.y, View.MeasureSpec.AT_MOST); + mPopupView.measure(specWidth, specHeight); + } + + public void setValue(CharSequence value) { + mPopupView.mMarker.setValue(value); + } + + public boolean isShowing() { + return mShowing; + } + + public void showIndicator(View parent, Rect touchBounds) { + if (isShowing()) { + mPopupView.mMarker.animateOpen(); + return; + } + + IBinder windowToken = parent.getWindowToken(); + if (windowToken != null) { + WindowManager.LayoutParams p = createPopupLayout(windowToken); + + p.gravity = Gravity.TOP | GravityCompat.START; + updateLayoutParamsForPosiion(parent, p, touchBounds.bottom); + mShowing = true; + + translateViewIntoPosition(touchBounds.centerX()); + invokePopup(p); + } + } + + public void move(int x) { + if (!isShowing()) { + return; + } + translateViewIntoPosition(x); + } + + public void setColors(int startColor, int endColor) { + mPopupView.setColors(startColor, endColor); + } + + /** + * This will start the closing animation of the Marker and call onClosingComplete when finished + */ + public void dismiss() { + mPopupView.mMarker.animateClose(); + } + + /** + * FORCE the popup window to be removed. + * You typically calls this when the parent view is being removed from the window to avoid a Window Leak + */ + public void dismissComplete() { + if (isShowing()) { + mShowing = false; + try { + mWindowManager.removeViewImmediate(mPopupView); + } finally { + } + } + } + + private void updateLayoutParamsForPosiion(View anchor, WindowManager.LayoutParams p, int yOffset) { + DisplayMetrics displayMetrics = anchor.getResources().getDisplayMetrics(); + screenSize.set(displayMetrics.widthPixels, displayMetrics.heightPixels); + + measureFloater(); + int measuredHeight = mPopupView.getMeasuredHeight(); + int paddingBottom = mPopupView.mMarker.getPaddingBottom(); + anchor.getLocationInWindow(mDrawingLocation); + p.x = 0; + p.y = mDrawingLocation[1] - measuredHeight + yOffset + paddingBottom; + p.width = screenSize.x; + p.height = measuredHeight; + } + + private void translateViewIntoPosition(final int x) { + mPopupView.setFloatOffset(x + mDrawingLocation[0]); + } + + private void invokePopup(WindowManager.LayoutParams p) { + mWindowManager.addView(mPopupView, p); + mPopupView.mMarker.animateOpen(); + } + + private WindowManager.LayoutParams createPopupLayout(IBinder token) { + WindowManager.LayoutParams p = new WindowManager.LayoutParams(); + p.gravity = Gravity.START | Gravity.TOP; + p.width = ViewGroup.LayoutParams.MATCH_PARENT; + p.height = ViewGroup.LayoutParams.MATCH_PARENT; + p.format = PixelFormat.TRANSLUCENT; + p.flags = computeFlags(p.flags); + p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + p.token = token; + p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; + p.setTitle("DiscreteSeekBar Indicator:" + Integer.toHexString(hashCode())); + + return p; + } + + /** + * I'm NOT completely sure how all this bitwise things work... + * + * @param curFlags + * @return + */ + private int computeFlags(int curFlags) { + curFlags &= ~( + WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; + curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + return curFlags; + } + + /** + * Small FrameLayout class to hold and move the bubble around when requested + * I wanted to use the {@link Marker} directly + * but doing so would make some things harder to implement + * (like moving the marker around, having the Marker's outline to work, etc) + */ + private class Floater extends FrameLayout implements MarkerDrawable.MarkerAnimationListener { + private Marker mMarker; + private int mOffset; + + public Floater(Context context, AttributeSet attrs, int defStyleAttr, String maxValue, int thumbSize, int separation) { + super(context); + mMarker = new Marker(context, attrs, defStyleAttr, maxValue, thumbSize, separation); + addView(mMarker, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + measureChildren(widthMeasureSpec, heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSie = mMarker.getMeasuredHeight(); + setMeasuredDimension(widthSize, heightSie); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int centerDiffX = mMarker.getMeasuredWidth() / 2; + int offset = (mOffset - centerDiffX); + mMarker.layout(offset, 0, offset + mMarker.getMeasuredWidth(), mMarker.getMeasuredHeight()); + } + + public void setFloatOffset(int x) { + mOffset = x; + int centerDiffX = mMarker.getMeasuredWidth() / 2; + int offset = (x - centerDiffX); + mMarker.offsetLeftAndRight(offset - mMarker.getLeft()); + //Without hardware acceleration (or API levels<11), offsetting a view seems to NOT invalidate the proper area. + //We should calc the proper invalidate Rect but this will be for now... + if (isHardwareAccelerated()) { + invalidate(); + } + } + + @Override + public void onClosingComplete() { + if (mListener != null) { + mListener.onClosingComplete(); + } + dismissComplete(); + } + + @Override + public void onOpeningComplete() { + if (mListener != null) { + mListener.onOpeningComplete(); + } + } + + public void setColors(int startColor, int endColor) { + mMarker.setColors(startColor, endColor); + } + } + +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/compat/SeekBarCompat.java b/library/src/main/java/com/mardous/discreteseekbar/internal/compat/SeekBarCompat.java new file mode 100644 index 0000000..b66009d --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/compat/SeekBarCompat.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal.compat; + +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import android.view.View; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.graphics.drawable.DrawableCompat; +import com.mardous.discreteseekbar.internal.drawable.AlmostRippleDrawable; +import com.mardous.discreteseekbar.internal.drawable.MarkerDrawable; + +/** + * Wrapper compatibility class to call some API-Specific methods + * And offer alternate procedures when possible + * + * @hide + */ +public class SeekBarCompat { + + /** + * Sets the custom Outline provider on API>=21. + * Does nothing on API<21 + * + * @param view + * @param markerDrawable + */ + public static void setOutlineProvider(View view, final MarkerDrawable markerDrawable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + SeekBarCompatDontCrash.setOutlineProvider(view, markerDrawable); + } + } + + /** + * Our DiscreteSeekBar implementation uses a circular drawable on API < 21 + * because we don't set it as Background, but draw it ourselves + * + * @param colorStateList + * @return + */ + public static Drawable getRipple(ColorStateList colorStateList) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return SeekBarCompatDontCrash.getRipple(colorStateList); + } else { + return new AlmostRippleDrawable(colorStateList); + } + } + + /** + * Sets the color of the seekbar ripple + * @param drawable + * @param colorStateList The ColorStateList the track ripple will be changed to + */ + public static void setRippleColor(@NonNull Drawable drawable, ColorStateList colorStateList) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ((RippleDrawable) drawable).setColor(colorStateList); + } else { + ((AlmostRippleDrawable) drawable).setColor(colorStateList); + } + } + + /** + * As our DiscreteSeekBar implementation uses a circular drawable on API < 21 + * we want to use the same method to set its bounds as the Ripple's hotspot bounds. + * + * @param drawable + * @param left + * @param top + * @param right + * @param bottom + */ + public static void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + //We don't want the full size rect, Lollipop ripple would be too big + int size = (right - left) / 8; + DrawableCompat.setHotspotBounds(drawable, left + size, top + size, right - size, bottom - size); + } else { + drawable.setBounds(left, top, right, bottom); + } + } + + /** + * Sets the TextView text direction attribute when possible + * + * @param textView + * @param textDirection + * @see android.widget.TextView#setTextDirection(int) + */ + public static void setTextDirection(TextView textView, int textDirection) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + SeekBarCompatDontCrash.setTextDirection(textView, textDirection); + } + } +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/compat/SeekBarCompatDontCrash.java b/library/src/main/java/com/mardous/discreteseekbar/internal/compat/SeekBarCompatDontCrash.java new file mode 100644 index 0000000..6a9aa37 --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/compat/SeekBarCompatDontCrash.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal.compat; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.graphics.Outline; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.TextView; +import com.mardous.discreteseekbar.internal.drawable.MarkerDrawable; + +/** + * Wrapper compatibility class to call some API-Specific methods + * And offer alternate procedures when possible + * + * @hide + */ +@TargetApi(21) +class SeekBarCompatDontCrash { + public static void setOutlineProvider(View marker, final MarkerDrawable markerDrawable) { + marker.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setConvexPath(markerDrawable.getPath()); + } + }); + } + + public static Drawable getRipple(ColorStateList colorStateList) { + return new RippleDrawable(colorStateList, null, null); + } + + public static void setTextDirection(TextView number, int textDirection) { + number.setTextDirection(textDirection); + } +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/AlmostRippleDrawable.java b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/AlmostRippleDrawable.java new file mode 100644 index 0000000..1e15d65 --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/AlmostRippleDrawable.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.os.SystemClock; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import androidx.annotation.NonNull; + +public class AlmostRippleDrawable extends StateDrawable implements Animatable { + private static final long FRAME_DURATION = 1000 / 60; + private static final int ANIMATION_DURATION = 250; + + private static final float INACTIVE_SCALE = 0f; + private static final float ACTIVE_SCALE = 1f; + private float mCurrentScale = INACTIVE_SCALE; + private Interpolator mInterpolator; + private long mStartTime; + private boolean mReverse = false; + private boolean mRunning = false; + private int mDuration = ANIMATION_DURATION; + private float mAnimationInitialValue; + //We don't use colors just with our drawable state because of animations + private int mPressedColor; + private int mFocusedColor; + private int mDisabledColor; + private int mRippleColor; + private int mRippleBgColor; + + public AlmostRippleDrawable(@NonNull ColorStateList tintStateList) { + super(tintStateList); + mInterpolator = new AccelerateDecelerateInterpolator(); + setColor(tintStateList); + } + + public void setColor(@NonNull ColorStateList tintStateList) { + int defaultColor = tintStateList.getDefaultColor(); + mFocusedColor = tintStateList.getColorForState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, defaultColor); + mPressedColor = tintStateList.getColorForState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, defaultColor); + mDisabledColor = tintStateList.getColorForState(new int[]{-android.R.attr.state_enabled}, defaultColor); + + //The ripple should be partially transparent + mFocusedColor = getModulatedAlphaColor(130, mFocusedColor); + mPressedColor = getModulatedAlphaColor(130, mPressedColor); + mDisabledColor = getModulatedAlphaColor(130, mDisabledColor); + } + + private static int getModulatedAlphaColor(int alphaValue, int originalColor) { + int alpha = Color.alpha(originalColor); + int scale = alphaValue + (alphaValue >> 7); + alpha = alpha * scale >> 8; + return Color.argb(alpha, Color.red(originalColor), Color.green(originalColor), Color.blue(originalColor)); + } + + @Override + public void doDraw(Canvas canvas, Paint paint) { + Rect bounds = getBounds(); + int size = Math.min(bounds.width(), bounds.height()); + float scale = mCurrentScale; + int rippleColor = mRippleColor; + int bgColor = mRippleBgColor; + float radius = (size / 2); + float radiusAnimated = radius * scale; + if (scale > INACTIVE_SCALE) { + if (bgColor != 0) { + paint.setColor(bgColor); + paint.setAlpha(decreasedAlpha(Color.alpha(bgColor))); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, paint); + } + if (rippleColor != 0) { + paint.setColor(rippleColor); + paint.setAlpha(modulateAlpha(Color.alpha(rippleColor))); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), radiusAnimated, paint); + } + } + } + + private int decreasedAlpha(int alpha) { + int scale = 100 + (100 >> 7); + return alpha * scale >> 8; + } + + @Override + public boolean setState(int[] stateSet) { + int[] oldState = getState(); + boolean oldPressed = false; + for (int i : oldState) { + if (i == android.R.attr.state_pressed) { + oldPressed = true; + } + } + super.setState(stateSet); + boolean focused = false; + boolean pressed = false; + boolean disabled = true; + for (int i : stateSet) { + if (i == android.R.attr.state_focused) { + focused = true; + } else if (i == android.R.attr.state_pressed) { + pressed = true; + } else if (i == android.R.attr.state_enabled) { + disabled = false; + } + } + + if (disabled) { + unscheduleSelf(mUpdater); + mRippleColor = mDisabledColor; + mRippleBgColor = 0; + mCurrentScale = ACTIVE_SCALE / 2; + invalidateSelf(); + } else { + if (pressed) { + animateToPressed(); + mRippleColor = mRippleBgColor = mPressedColor; + } else if (oldPressed) { + mRippleColor = mRippleBgColor = mPressedColor; + animateToNormal(); + } else if (focused) { + mRippleColor = mFocusedColor; + mRippleBgColor = 0; + mCurrentScale = ACTIVE_SCALE; + invalidateSelf(); + } else { + mRippleColor = 0; + mRippleBgColor = 0; + mCurrentScale = INACTIVE_SCALE; + invalidateSelf(); + } + } + return true; + } + + public void animateToPressed() { + unscheduleSelf(mUpdater); + if (mCurrentScale < ACTIVE_SCALE) { + mReverse = false; + mRunning = true; + mAnimationInitialValue = mCurrentScale; + float durationFactor = 1f - ((mAnimationInitialValue - INACTIVE_SCALE) / (ACTIVE_SCALE - INACTIVE_SCALE)); + mDuration = (int) (ANIMATION_DURATION * durationFactor); + mStartTime = SystemClock.uptimeMillis(); + scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); + } + } + + public void animateToNormal() { + unscheduleSelf(mUpdater); + if (mCurrentScale > INACTIVE_SCALE) { + mReverse = true; + mRunning = true; + mAnimationInitialValue = mCurrentScale; + float durationFactor = 1f - ((mAnimationInitialValue - ACTIVE_SCALE) / (INACTIVE_SCALE - ACTIVE_SCALE)); + mDuration = (int) (ANIMATION_DURATION * durationFactor); + mStartTime = SystemClock.uptimeMillis(); + scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); + } + } + + private void updateAnimation(float factor) { + float initial = mAnimationInitialValue; + float destination = mReverse ? INACTIVE_SCALE : ACTIVE_SCALE; + mCurrentScale = initial + (destination - initial) * factor; + invalidateSelf(); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + + long currentTime = SystemClock.uptimeMillis(); + long diff = currentTime - mStartTime; + if (diff < mDuration) { + float interpolation = mInterpolator.getInterpolation((float) diff / (float) mDuration); + scheduleSelf(mUpdater, currentTime + FRAME_DURATION); + updateAnimation(interpolation); + } else { + unscheduleSelf(mUpdater); + mRunning = false; + updateAnimation(1f); + } + } + }; + + @Override + public void start() { + //No-Op. We control our own animation + } + + @Override + public void stop() { + //No-Op. We control our own animation + } + + @Override + public boolean isRunning() { + return mRunning; + } +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/MarkerDrawable.java b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/MarkerDrawable.java new file mode 100644 index 0000000..d4095ce --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/MarkerDrawable.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Animatable; +import android.os.SystemClock; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import androidx.annotation.NonNull; + +/** + * Implementation of {@link StateDrawable} to draw a morphing marker symbol. + *+ * It's basically an implementation of an {@link android.graphics.drawable.Animatable} Drawable with the following details: + *
+ *+ * Subclasses should implement {@link #doDraw(android.graphics.Canvas, android.graphics.Paint)} + *
+ * + * @hide + */ +public abstract class StateDrawable extends Drawable { + private ColorStateList mTintStateList; + private final Paint mPaint; + private int mCurrentColor; + private int mAlpha = 255; + + public StateDrawable(@NonNull ColorStateList tintStateList) { + super(); + setColorStateList(tintStateList); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + @Override + public boolean isStateful() { + return (mTintStateList.isStateful()) || super.isStateful(); + } + + @Override + public boolean setState(int[] stateSet) { + boolean handled = super.setState(stateSet); + handled = updateTint(stateSet) || handled; + return handled; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + private boolean updateTint(int[] state) { + final int color = mTintStateList.getColorForState(state, mCurrentColor); + if (color != mCurrentColor) { + mCurrentColor = color; + //We've changed states + invalidateSelf(); + return true; + } + return false; + } + + @Override + public void draw(Canvas canvas) { + mPaint.setColor(mCurrentColor); + int alpha = modulateAlpha(Color.alpha(mCurrentColor)); + mPaint.setAlpha(alpha); + doDraw(canvas, mPaint); + } + + public void setColorStateList(@NonNull ColorStateList tintStateList) { + mTintStateList = tintStateList; + mCurrentColor = tintStateList.getDefaultColor(); + } + + /** + * Subclasses should implement this method to do the actual drawing + * + * @param canvas The current {@link android.graphics.Canvas} to draw into + * @param paint The {@link android.graphics.Paint} preconfigurred with the current + * {@link android.content.res.ColorStateList} color + */ + abstract void doDraw(Canvas canvas, Paint paint); + + @Override + public void setAlpha(int alpha) { + mAlpha = alpha; + invalidateSelf(); + } + + int modulateAlpha(int alpha) { + int scale = mAlpha + (mAlpha >> 7); + return alpha * scale >> 8; + } + + @Override + public int getAlpha() { + return mAlpha; + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/ThumbDrawable.java b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/ThumbDrawable.java new file mode 100644 index 0000000..4d000e5 --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/ThumbDrawable.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.os.SystemClock; +import androidx.annotation.NonNull; + +/** + *+ * Special {@link com.mardous.discreteseekbar.internal.drawable.StateDrawable} implementation + * to draw the Thumb circle. + *
+ *+ * It's special because it will stop drawing once the state is pressed/focused BUT only after a small delay. + *
+ *+ * This special delay is meant to help avoiding frame glitches while the {@link com.mardous.discreteseekbar.internal.Marker} is added to the Window + *
+ * + * @hide + */ +public class ThumbDrawable extends StateDrawable implements Animatable { + //The current size for this drawable. Must be converted to real DPs + public static final int DEFAULT_SIZE_DP = 12; + private final int mSize; + private boolean mOpen; + private boolean mRunning; + + public ThumbDrawable(@NonNull ColorStateList tintStateList, int size) { + super(tintStateList); + mSize = size; + } + + @Override + public int getIntrinsicWidth() { + return mSize; + } + + @Override + public int getIntrinsicHeight() { + return mSize; + } + + @Override + public void doDraw(Canvas canvas, Paint paint) { + if (!mOpen) { + Rect bounds = getBounds(); + float radius = (mSize / 2); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, paint); + } + } + + public void animateToPressed() { + scheduleSelf(opener, SystemClock.uptimeMillis() + 100); + mRunning = true; + } + + public void animateToNormal() { + mOpen = false; + mRunning = false; + unscheduleSelf(opener); + invalidateSelf(); + } + + private Runnable opener = new Runnable() { + @Override + public void run() { + mOpen = true; + invalidateSelf(); + mRunning = false; + } + }; + + @Override + public void start() { + //NOOP + } + + @Override + public void stop() { + animateToNormal(); + } + + @Override + public boolean isRunning() { + return mRunning; + } +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/TrackOvalDrawable.java b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/TrackOvalDrawable.java new file mode 100644 index 0000000..0bde7fc --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/TrackOvalDrawable.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import androidx.annotation.NonNull; + +/** + * Simple {@link com.mardous.discreteseekbar.internal.drawable.StateDrawable} implementation + * to draw circles/ovals + * + * @hide + */ +public class TrackOvalDrawable extends StateDrawable { + private RectF mRectF = new RectF(); + + public TrackOvalDrawable(@NonNull ColorStateList tintStateList) { + super(tintStateList); + } + + @Override + void doDraw(Canvas canvas, Paint paint) { + mRectF.set(getBounds()); + canvas.drawOval(mRectF, paint); + } + +} diff --git a/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/TrackRectDrawable.java b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/TrackRectDrawable.java new file mode 100644 index 0000000..ba30933 --- /dev/null +++ b/library/src/main/java/com/mardous/discreteseekbar/internal/drawable/TrackRectDrawable.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. + * + * 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.mardous.discreteseekbar.internal.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Paint; +import androidx.annotation.NonNull; + +/** + * Simple {@link com.mardous.discreteseekbar.internal.drawable.StateDrawable} implementation + * to draw rectangles + * + * @hide + */ +public class TrackRectDrawable extends StateDrawable { + public TrackRectDrawable(@NonNull ColorStateList tintStateList) { + super(tintStateList); + } + + @Override + void doDraw(Canvas canvas, Paint paint) { + canvas.drawRect(getBounds(), paint); + } + +} diff --git a/library/src/main/res/color/dsb_progress_color_list.xml b/library/src/main/res/color/dsb_progress_color_list.xml new file mode 100644 index 0000000..bb5d5ac --- /dev/null +++ b/library/src/main/res/color/dsb_progress_color_list.xml @@ -0,0 +1,21 @@ + + + +