Last active
August 29, 2015 14:07
-
-
Save corsair992/a0c39530ac32da9b50f4 to your computer and use it in GitHub Desktop.
This is a derivative of the Android SeekBar widget that supports an indicator attached above the thumb. It extends SeekBar for convenience even though most of the functionality in it's base class AbsSeekBar was defined in private methods, and needed to be copied/hacked. The comments are also copied from AbsSeekBar except where evident. The app t…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.test.widget; | |
import static android.os.Build.VERSION.SDK_INT; | |
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; | |
import android.annotation.TargetApi; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.graphics.Canvas; | |
import android.graphics.Rect; | |
import android.graphics.drawable.Drawable; | |
import android.util.AttributeSet; | |
import android.widget.SeekBar; | |
import com.example.test.R; | |
public class AttachableSeekBar extends SeekBar { | |
private static final int MAX_LEVEL = 10000; | |
int minWidth, maxWidth, minHeight, maxHeight; | |
boolean mirrorForRtl; | |
private Drawable thumb, indicator; | |
private OnSeekBarChangeListener changeListener; { | |
super.setOnSeekBarChangeListener(new SelfChangeListener()); | |
} | |
public AttachableSeekBar(Context context) { | |
this(context, null); | |
} | |
public AttachableSeekBar(Context context, AttributeSet attrs) { | |
this(context, attrs, R.attr.attachableSeekBarStyle); | |
} | |
public AttachableSeekBar(Context context, AttributeSet attrs, int defStyle) { | |
super(context, attrs, defStyle); | |
TypedArray a = context.obtainStyledAttributes(attrs, | |
R.styleable.AttachableSeekBar, defStyle, R.style.Widget_AttachableSeekBar); | |
minWidth = a.getDimensionPixelSize(R.styleable.AttachableSeekBar_android_minWidth, minWidth); | |
maxWidth = a.getDimensionPixelSize(R.styleable.AttachableSeekBar_android_maxWidth, maxWidth); | |
minHeight = a.getDimensionPixelSize(R.styleable.AttachableSeekBar_android_minHeight, minHeight); | |
maxHeight = a.getDimensionPixelSize(R.styleable.AttachableSeekBar_android_maxHeight, maxHeight); | |
mirrorForRtl = a.getBoolean(R.styleable.AttachableSeekBar_android_mirrorForRtl, mirrorForRtl); | |
setIndicator(a.getDrawable(R.styleable.AttachableSeekBar_indicator)); | |
setThumbOffset(a.getDimensionPixelOffset( | |
R.styleable.AttachableSeekBar_android_thumbOffset, getThumbOffset())); | |
a.recycle(); | |
} | |
@Override | |
protected synchronized void onDraw(Canvas canvas) { | |
super.onDraw(canvas); | |
if (indicator != null) { | |
canvas.save(); | |
// Translate the padding. For the x, we need to allow the thumb to | |
// draw in its extra space | |
canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop()); | |
indicator.draw(canvas); | |
canvas.restore(); | |
} | |
} | |
@Override | |
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
Drawable d = getCurrentDrawable(); | |
int dw = 0, dh = 0; | |
if (d != null) { | |
dw = Math.max(minWidth, Math.min(maxWidth, d.getIntrinsicWidth())); | |
dh = Math.max(minHeight, Math.min(maxHeight, d.getIntrinsicHeight())); | |
if (thumb != null) { | |
int thumbHeight = thumb.getIntrinsicHeight(); | |
if (thumbHeight > dh) { | |
dh = thumbHeight; | |
if (indicator != null) { | |
dh += indicator.getIntrinsicHeight(); | |
} | |
} else if (indicator != null) { | |
int indicatorHeight = indicator.getIntrinsicHeight(); | |
float dhHalf = dh / 2f, thumbHeightHalf = thumbHeight / 2f; | |
if (dhHalf < thumbHeightHalf + indicatorHeight) { | |
dh = ((int) (dhHalf + thumbHeightHalf)) + indicatorHeight; | |
} | |
} | |
} | |
} | |
dw += getPaddingLeft() + getPaddingRight(); | |
dh += getPaddingTop() + getPaddingBottom(); | |
setMeasuredDimension(resolveSize(dw, widthMeasureSpec), | |
resolveSize(dh, heightMeasureSpec)); | |
} | |
@Override | |
protected void onSizeChanged(int w, int h, int oldw, int oldh) { | |
super.onSizeChanged(w, h, oldw, oldh); | |
updateThumbPos(w, h); | |
} | |
// Making this private instead of package-private because strangely | |
// doing that causes this method to be called from the super-class, | |
// but without the possibility of calling through to the super implementation. | |
private void onProgressRefresh(float scale, boolean fromUser) { | |
if (thumb != null) { | |
setThumbPos(getWidth(), getHeight(), scale, Integer.MIN_VALUE); | |
int level = (int) (scale * MAX_LEVEL); | |
thumb.setLevel(level); | |
if (indicator != null) { | |
indicator.setLevel(level); | |
} | |
invalidate(); | |
} | |
} | |
private void updateThumbPos(int w, int h) { | |
Drawable d = getCurrentDrawable(); | |
int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); | |
// The max height does not incorporate padding, whereas the height | |
// parameter does | |
int paddingTop = getPaddingTop(), paddingBottom = getPaddingBottom(), | |
paddingLeft = getPaddingLeft(), paddingRight = getPaddingRight(); | |
int trackHeight = Math.min(maxHeight, h - paddingTop - paddingBottom); | |
int max = getMax(); | |
float scale = max > 0 ? (float) getProgress() / (float) max : 0; | |
if (thumbHeight > trackHeight) { | |
if (thumb != null) { | |
setThumbPos(w, h, scale, 0); | |
} | |
if (d != null) { | |
int gapForCenteringTrack = (thumbHeight - trackHeight) / 2; | |
// Canvas will be translated by the padding, so 0,0 is where we start drawing | |
d.setBounds(0, gapForCenteringTrack, | |
w - paddingRight - paddingLeft, h - paddingBottom - gapForCenteringTrack | |
- paddingTop); | |
} | |
} else { | |
int dh = d == null ? 0 : d.getIntrinsicHeight(); | |
int barHeight = Math.max(dh, thumbHeight); | |
int indicatorHeight = indicator == null ? 0 : indicator.getIntrinsicHeight(); | |
int availableHeight = h - paddingBottom - paddingTop; | |
int contentHeight = Math.max(availableHeight, barHeight + indicatorHeight); | |
int contentBottom = (int) ((availableHeight / 2f) + (contentHeight / 2f)); | |
float barCenter = contentBottom - (barHeight / 2f); | |
if (d != null) { | |
float dhHalf = dh / 2f; | |
d.setBounds(0, (int) (barCenter - dhHalf), | |
w - paddingRight - paddingLeft, (int) (barCenter + dhHalf)); | |
} | |
if (thumb != null) { | |
int gap = availableHeight - contentBottom; | |
if (thumbHeight < dh) { | |
gap += (dh - thumbHeight) / 2; | |
} | |
setThumbPos(w, h, scale, gap); | |
} | |
} | |
} | |
/** | |
* @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and | |
*/ | |
@TargetApi(JELLY_BEAN_MR1) | |
private void setThumbPos(int w, int h, float scale, int gap) { | |
int thumbWidth = thumb.getIntrinsicWidth(); | |
int indicatorWidth = indicator == null ? 0 : indicator.getIntrinsicWidth(); | |
int availableWidth = w - getPaddingLeft() - getPaddingRight() - | |
Math.max(thumbWidth, indicatorWidth) + (getThumbOffset() * 2); | |
int thumbPos = (int) (scale * availableWidth); | |
// Canvas will be translated, so 0,0 is where we start drawing | |
final int left = SDK_INT >= JELLY_BEAN_MR1 && | |
getLayoutDirection() == LAYOUT_DIRECTION_RTL && mirrorForRtl ? | |
availableWidth - thumbPos : thumbPos; | |
int thumbLeft = left, indicatorLeft = left; | |
if (thumbWidth < indicatorWidth) { | |
thumbLeft += (indicatorWidth - thumbWidth) / 2; | |
} else if (indicatorWidth < thumbWidth) { | |
indicatorLeft += (thumbWidth - indicatorWidth) / 2; | |
} | |
if (gap == Integer.MIN_VALUE) { | |
Rect oldBounds = thumb.getBounds(); | |
thumb.setBounds(thumbLeft, oldBounds.top, | |
thumbLeft + thumbWidth, oldBounds.bottom); | |
if (indicator != null) { | |
oldBounds = indicator.getBounds(); | |
indicator.setBounds(indicatorLeft, oldBounds.top, | |
indicatorLeft + indicatorWidth, oldBounds.bottom); | |
} | |
} else { | |
int availableHeight = h - getPaddingTop() - getPaddingBottom() - gap; | |
int thumbHeight = thumb.getIntrinsicHeight(); | |
if (thumbHeight >= availableHeight) { | |
thumb.setBounds(thumbLeft, gap, thumbLeft + thumbWidth, gap + thumb.getIntrinsicHeight()); | |
if (indicator != null) { | |
indicator.setBounds(0, 0, 0, 0); | |
} | |
} else { | |
int thumbTop = availableHeight - thumbHeight; | |
thumb.setBounds(thumbLeft, thumbTop, thumbLeft + thumbWidth, availableHeight); | |
if (indicator != null) { | |
indicator.setBounds(indicatorLeft, thumbTop - indicator.getIntrinsicHeight(), | |
indicatorLeft + indicatorWidth, thumbTop); | |
} | |
} | |
} | |
} | |
// Making this private instead of package-private for the same reason | |
// as in onProgressRefresh(). | |
private Drawable getCurrentDrawable() { | |
return isIndeterminate() ? getIndeterminateDrawable() : getProgressDrawable(); | |
} | |
// Overriding to provide a consistent API across different API levels, | |
// as with the getIndicator() method. | |
@Override | |
public Drawable getThumb() { | |
return thumb; | |
} | |
@Override | |
public void setThumb(Drawable thumb) { | |
super.setThumb(thumb); | |
if (thumb != null) { | |
setDefaultThumbOffset(thumb, indicator); | |
int max = getMax(); | |
thumb.setLevel(max > 0 ? (int) ((getProgress() / (float) max) * MAX_LEVEL) : 0); | |
} | |
boolean needUpdate = this.thumb != null && thumb != this.thumb; | |
this.thumb = thumb; | |
invalidate(); | |
if (needUpdate) { | |
updateThumbPos(getWidth(), getHeight()); | |
} | |
} | |
public Drawable getIndicator() { | |
return indicator; | |
} | |
public void setIndicator(Drawable indicator) { | |
boolean needUpdate; | |
// This way, calling setIndicator again with the same bitmap will result in | |
// it recalcuating the thumb offset (if for example it the bounds of the | |
// drawable changed) | |
if (this.indicator != null && indicator != this.indicator) { | |
this.indicator.setCallback(null); | |
needUpdate = true; | |
} else { | |
needUpdate = false; | |
} | |
setDefaultThumbOffset(thumb, indicator); | |
if (indicator != null) { | |
indicator.setCallback(this); | |
int max = getMax(); | |
indicator.setLevel(max > 0 ? (int) ((getProgress() / (float) max) * MAX_LEVEL) : 0); | |
// If we're updating get the new states | |
if (needUpdate && indicator.getIntrinsicHeight() != | |
this.indicator.getIntrinsicHeight()) { | |
requestLayout(); | |
} | |
} | |
this.indicator = indicator; | |
invalidate(); | |
if (needUpdate) { | |
updateThumbPos(getWidth(), getHeight()); | |
if (indicator != null && indicator.isStateful()) { | |
// Note that if the states are different this won't work. | |
// For now, let's consider that an app bug. | |
indicator.setState(getDrawableState()); | |
} | |
} | |
} | |
private void setDefaultThumbOffset(Drawable thumb, Drawable indicator) { | |
if (thumb == null) return; | |
int thumbWidth = thumb.getIntrinsicWidth(); | |
if (indicator != null) { | |
thumbWidth = Math.max(thumbWidth, indicator.getIntrinsicWidth()); | |
} | |
setThumbOffset(thumbWidth / 2); | |
} | |
@Override | |
protected boolean verifyDrawable(Drawable who) { | |
return who == indicator || super.verifyDrawable(who); | |
} | |
@Override | |
public void jumpDrawablesToCurrentState() { | |
super.jumpDrawablesToCurrentState(); | |
if (indicator != null) indicator.jumpToCurrentState(); | |
} | |
@Override | |
protected void drawableStateChanged() { | |
super.drawableStateChanged(); | |
if (indicator != null && indicator.isStateful()) { | |
indicator.setState(getDrawableState()); | |
} | |
} | |
@Override | |
public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { | |
changeListener = l; | |
} | |
@Override | |
@TargetApi(JELLY_BEAN_MR1) | |
public void onRtlPropertiesChanged(int layoutDirection) { | |
super.onRtlPropertiesChanged(layoutDirection); | |
int max = getMax(); | |
float scale = max > 0 ? (float) getProgress() / (float) max : 0; | |
if (thumb != null) { | |
int width = getWidth(), height = getHeight(); | |
if (width > 0 && height > 0) { | |
setThumbPos(width, height, scale, Integer.MIN_VALUE); | |
/* | |
* Since we draw translated, the drawable's bounds that it signals | |
* for invalidation won't be the actual bounds we want invalidated, | |
* so just invalidate this whole view. | |
*/ | |
invalidate(); | |
} | |
} | |
} | |
private static class SelfChangeListener implements OnSeekBarChangeListener { | |
@Override | |
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { | |
AttachableSeekBar attachableSeekBar = (AttachableSeekBar)seekBar; | |
int max = attachableSeekBar.getMax(); | |
attachableSeekBar.onProgressRefresh(max > 0 ? progress / (float) max : 0, fromUser); | |
if (attachableSeekBar.changeListener != null) | |
attachableSeekBar.changeListener.onProgressChanged(seekBar, progress, fromUser); | |
} | |
@Override | |
public void onStartTrackingTouch(SeekBar seekBar) { | |
AttachableSeekBar attachableSeekBar = (AttachableSeekBar)seekBar; | |
if (attachableSeekBar.changeListener != null) | |
attachableSeekBar.changeListener.onStartTrackingTouch(seekBar); | |
} | |
@Override | |
public void onStopTrackingTouch(SeekBar seekBar) { | |
AttachableSeekBar attachableSeekBar = (AttachableSeekBar)seekBar; | |
if (attachableSeekBar.changeListener != null) | |
attachableSeekBar.changeListener.onStopTrackingTouch(seekBar); | |
} | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<declare-styleable name="Theme"> | |
<attr name="attachableSeekBarStyle" format="reference" /> | |
</declare-styleable> | |
<declare-styleable name="AttachableSeekBar"> | |
<attr name="android:minWidth" /> | |
<attr name="android:maxWidth" /> | |
<attr name="android:minHeight" /> | |
<attr name="android:maxHeight" /> | |
<attr name="android:mirrorForRtl" /> | |
<attr name="android:thumbOffset" /> | |
<attr name="indicator" format="reference" /> | |
</declare-styleable> | |
</resources> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment