Skip to content

Instantly share code, notes, and snippets.

@corsair992
Last active August 29, 2015 14:07
Show Gist options
  • Save corsair992/a0c39530ac32da9b50f4 to your computer and use it in GitHub Desktop.
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…
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);
}
};
}
<?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