package bf.io.openshop.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ImageView;
import java.math.BigDecimal;
import bf.io.openshop.R;
import bf.io.openshop.utils.Utils;
/**
* Widget that lets users select a minimum and maximum value on a given numerical range.
* The range value types can be one of Long, Double, Integer, Float, Short, Byte or BigDecimal.<br>
* <br>
* Improved {@link android.view.MotionEvent} handling for smoother use, anti-aliased painting for improved aesthetics.
*
* @param <T> The Number type of the range values. One of Long, Double, Integer, Float, Short, Byte or BigDecimal.
* @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
* @author Peter Sinnott (psinnott@gmail.com)
* @author Thomas Barrasso (tbarrasso@sevenplusandroid.org)
* @author Alex Florescu (alex@florescu.org)
* @author Michael Keppler (bananeweizen@gmx.de)
*/
public class RangeSeekBar<T extends Number> extends ImageView {
/**
* Default color of a {@link RangeSeekBar}, #FFee1f65.
*/
public static final int ACTIVE_COLOR = Color.argb(0xFF, 0xee, 0x1f, 0x65);
/**
* An invalid pointer id.
*/
public static final int INVALID_POINTER_ID = 255;
// Localized constants from MotionEvent for compatibility
// with API < 8 "Froyo".
public static final int ACTION_POINTER_INDEX_MASK = 0x0000ff00, ACTION_POINTER_INDEX_SHIFT = 8;
public static final Integer DEFAULT_MINIMUM = 0;
public static final Integer DEFAULT_MAXIMUM = 100;
public static final int HEIGHT_IN_DP = 30;
public static final int TEXT_LATERAL_PADDING_IN_DP = 3;
private static final int INITIAL_PADDING_IN_DP = 8;
private static final int DEFAULT_TEXT_SIZE_IN_DP = 14;
private static final int DEFAULT_TEXT_DISTANCE_TO_BUTTON_IN_DP = 8;
private static final int DEFAULT_TEXT_DISTANCE_TO_TOP_IN_DP = 8;
private static final int LINE_HEIGHT_IN_DP = 1;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint shadowPaint = new Paint();
private Bitmap thumbImage;
private Bitmap thumbPressedImage;
private Bitmap thumbDisabledImage;
private float mThumbHalfWidth;
private float mThumbHalfHeight;
private float padding;
private T absoluteMinValue, absoluteMaxValue;
private NumberType numberType;
private double absoluteMinValuePrim, absoluteMaxValuePrim;
private double normalizedMinValue = 0d;
private double normalizedMaxValue = 1d;
private Thumb pressedThumb = null;
private boolean notifyWhileDragging = false;
private OnRangeSeekBarChangeListener<T> listener;
private float mDownMotionX;
private int mActivePointerId = INVALID_POINTER_ID;
private int mScaledTouchSlop;
private boolean mIsDragging;
private int mTextOffset;
private int mTextSize;
private int mDistanceToTop;
private RectF mRect;
private boolean mSingleThumb;
private boolean mAlwaysActive;
private boolean mShowLabels;
private boolean mShowTextAboveThumbs;
private float mInternalPad;
private int mActiveColor;
private int mDefaultColor;
private int mTextAboveThumbsColor;
private boolean mThumbShadow;
private int mThumbShadowXOffset;
private int mThumbShadowYOffset;
private int mThumbShadowBlur;
private Path mThumbShadowPath;
private Path mTranslatedThumbShadowPath = new Path();
private Matrix mThumbShadowMatrix = new Matrix();
private boolean mActivateOnDefaultValues;
public RangeSeekBar(Context context) {
super(context);
init(context, null);
}
public RangeSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RangeSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
@SuppressWarnings("unchecked")
private T extractNumericValueFromAttributes(TypedArray a, int attribute, int defaultValue) {
TypedValue tv = a.peekValue(attribute);
if (tv == null) {
return (T) Integer.valueOf(defaultValue);
}
int type = tv.type;
if (type == TypedValue.TYPE_FLOAT) {
return (T) Float.valueOf(a.getFloat(attribute, defaultValue));
} else {
return (T) Integer.valueOf(a.getInteger(attribute, defaultValue));
}
}
private void init(Context context, AttributeSet attrs) {
float barHeight;
int thumbNormal = R.drawable.apptheme_scrubber_control_normal_holo;
int thumbPressed = R.drawable.apptheme_scrubber_control_pressed_holo;
int thumbDisabled = R.drawable.apptheme_scrubber_control_disabled_holo;
int thumbShadowColor;
int defaultShadowColor = Color.argb(75, 0, 0, 0);
int defaultShadowYOffset = Utils.dpToPx(context, 2);
int defaultShadowXOffset = Utils.dpToPx(context, 0);
int defaultShadowBlur = Utils.dpToPx(context, 2);
if (attrs == null) {
setRangeToDefaultValues();
mInternalPad = Utils.dpToPx(context, INITIAL_PADDING_IN_DP);
barHeight = Utils.dpToPx(context, LINE_HEIGHT_IN_DP);
mActiveColor = ACTIVE_COLOR;
mDefaultColor = Color.GRAY;
mAlwaysActive = false;
mShowTextAboveThumbs = true;
mTextAboveThumbsColor = Color.WHITE;
thumbShadowColor = defaultShadowColor;
mThumbShadowXOffset = defaultShadowXOffset;
mThumbShadowYOffset = defaultShadowYOffset;
mThumbShadowBlur = defaultShadowBlur;
mActivateOnDefaultValues = false;
} else {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RangeSeekBar, 0, 0);
try {
setRangeValues(
extractNumericValueFromAttributes(a, R.styleable.RangeSeekBar_absoluteMinValue, DEFAULT_MINIMUM),
extractNumericValueFromAttributes(a, R.styleable.RangeSeekBar_absoluteMaxValue, DEFAULT_MAXIMUM)
);
mShowTextAboveThumbs = a.getBoolean(R.styleable.RangeSeekBar_valuesAboveThumbs, true);
mTextAboveThumbsColor = a.getColor(R.styleable.RangeSeekBar_textAboveThumbsColor, Color.WHITE);
mSingleThumb = a.getBoolean(R.styleable.RangeSeekBar_singleThumb, false);
mShowLabels = a.getBoolean(R.styleable.RangeSeekBar_showLabels, true);
mInternalPad = a.getDimensionPixelSize(R.styleable.RangeSeekBar_internalPadding, INITIAL_PADDING_IN_DP);
barHeight = a.getDimensionPixelSize(R.styleable.RangeSeekBar_barHeight, LINE_HEIGHT_IN_DP);
mActiveColor = a.getColor(R.styleable.RangeSeekBar_activeColor, ACTIVE_COLOR);
mDefaultColor = a.getColor(R.styleable.RangeSeekBar_defaultColor, Color.GRAY);
mAlwaysActive = a.getBoolean(R.styleable.RangeSeekBar_alwaysActive, false);
Drawable normalDrawable = a.getDrawable(R.styleable.RangeSeekBar_thumbNormal);
if (normalDrawable != null) {
thumbImage = Utils.drawableToBitmap(normalDrawable);
}
Drawable disabledDrawable = a.getDrawable(R.styleable.RangeSeekBar_thumbDisabled);
if (disabledDrawable != null) {
thumbDisabledImage = Utils.drawableToBitmap(disabledDrawable);
}
Drawable pressedDrawable = a.getDrawable(R.styleable.RangeSeekBar_thumbPressed);
if (pressedDrawable != null) {
thumbPressedImage = Utils.drawableToBitmap(pressedDrawable);
}
mThumbShadow = a.getBoolean(R.styleable.RangeSeekBar_thumbShadow, false);
thumbShadowColor = a.getColor(R.styleable.RangeSeekBar_thumbShadowColor, defaultShadowColor);
mThumbShadowXOffset = a.getDimensionPixelSize(R.styleable.RangeSeekBar_thumbShadowXOffset, defaultShadowXOffset);
mThumbShadowYOffset = a.getDimensionPixelSize(R.styleable.RangeSeekBar_thumbShadowYOffset, defaultShadowYOffset);
mThumbShadowBlur = a.getDimensionPixelSize(R.styleable.RangeSeekBar_thumbShadowBlur, defaultShadowBlur);
mActivateOnDefaultValues = a.getBoolean(R.styleable.RangeSeekBar_activateOnDefaultValues, false);
} finally {
a.recycle();
}
}
if (thumbImage == null) {
thumbImage = BitmapFactory.decodeResource(getResources(), thumbNormal);
}
if (thumbPressedImage == null) {
thumbPressedImage = BitmapFactory.decodeResource(getResources(), thumbPressed);
}
if (thumbDisabledImage == null) {
thumbDisabledImage = BitmapFactory.decodeResource(getResources(), thumbDisabled);
}
mThumbHalfWidth = 0.5f * thumbImage.getWidth();
mThumbHalfHeight = 0.5f * thumbImage.getHeight();
setValuePrimAndNumberType();
mTextSize = Utils.dpToPx(context, DEFAULT_TEXT_SIZE_IN_DP);
mDistanceToTop = Utils.dpToPx(context, DEFAULT_TEXT_DISTANCE_TO_TOP_IN_DP);
mTextOffset = !mShowTextAboveThumbs ? 0 : this.mTextSize + Utils.dpToPx(context,
DEFAULT_TEXT_DISTANCE_TO_BUTTON_IN_DP) + this.mDistanceToTop;
mRect = new RectF(padding,
mTextOffset + mThumbHalfHeight - barHeight / 2,
getWidth() - padding,
mTextOffset + mThumbHalfHeight + barHeight / 2);
// make RangeSeekBar focusable. This solves focus handling issues in case EditText widgets are being used along with the RangeSeekBar within ScrollViews.
setFocusable(true);
setFocusableInTouchMode(true);
mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (mThumbShadow) {
// We need to remove hardware acceleration in order to blur the shadow
setLayerType(LAYER_TYPE_SOFTWARE, null);
shadowPaint.setColor(thumbShadowColor);
shadowPaint.setMaskFilter(new BlurMaskFilter(mThumbShadowBlur, BlurMaskFilter.Blur.NORMAL));
mThumbShadowPath = new Path();
mThumbShadowPath.addCircle(0,
0,
mThumbHalfHeight,
Path.Direction.CW);
}
}
public void setRangeValues(T minValue, T maxValue) {
this.absoluteMinValue = minValue;
this.absoluteMaxValue = maxValue;
setValuePrimAndNumberType();
}
public void setTextAboveThumbsColor(int textAboveThumbsColor) {
this.mTextAboveThumbsColor = textAboveThumbsColor;
invalidate();
}
public void setTextAboveThumbsColorResource(@ColorRes int resId) {
setTextAboveThumbsColor(getResources().getColor(resId));
}
@SuppressWarnings("unchecked")
// only used to set default values when initialised from XML without any values specified
private void setRangeToDefaultValues() {
this.absoluteMinValue = (T) DEFAULT_MINIMUM;
this.absoluteMaxValue = (T) DEFAULT_MAXIMUM;
setValuePrimAndNumberType();
}
private void setValuePrimAndNumberType() {
absoluteMinValuePrim = absoluteMinValue.doubleValue();
absoluteMaxValuePrim = absoluteMaxValue.doubleValue();
numberType = NumberType.fromNumber(absoluteMinValue);
}
@SuppressWarnings("unused")
public void resetSelectedValues() {
setSelectedMinValue(absoluteMinValue);
setSelectedMaxValue(absoluteMaxValue);
}
@SuppressWarnings("unused")
public boolean isNotifyWhileDragging() {
return notifyWhileDragging;
}
/**
* Should the widget notify the listener callback while the user is still dragging a thumb? Default is false.
*/
@SuppressWarnings("unused")
public void setNotifyWhileDragging(boolean flag) {
this.notifyWhileDragging = flag;
}
/**
* Returns the absolute minimum value of the range that has been set at construction time.
*
* @return The absolute minimum value of the range.
*/
public T getAbsoluteMinValue() {
return absoluteMinValue;
}
/**
* Returns the absolute maximum value of the range that has been set at construction time.
*
* @return The absolute maximum value of the range.
*/
public T getAbsoluteMaxValue() {
return absoluteMaxValue;
}
/**
* Returns the currently selected min value.
*
* @return The currently selected min value.
*/
public T getSelectedMinValue() {
return normalizedToValue(normalizedMinValue);
}
/**
* Sets the currently selected minimum value. The widget will be invalidated and redrawn.
*
* @param value The Number value to set the minimum value to. Will be clamped to given absolute minimum/maximum range.
*/
public void setSelectedMinValue(T value) {
// in case absoluteMinValue == absoluteMaxValue, avoid division by zero when normalizing.
if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) {
setNormalizedMinValue(0d);
} else {
setNormalizedMinValue(valueToNormalized(value));
}
}
/**
* Returns the currently selected max value.
*
* @return The currently selected max value.
*/
public T getSelectedMaxValue() {
return normalizedToValue(normalizedMaxValue);
}
/**
* Sets the currently selected maximum value. The widget will be invalidated and redrawn.
*
* @param value The Number value to set the maximum value to. Will be clamped to given absolute minimum/maximum range.
*/
public void setSelectedMaxValue(T value) {
// in case absoluteMinValue == absoluteMaxValue, avoid division by zero when normalizing.
if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) {
setNormalizedMaxValue(1d);
} else {
setNormalizedMaxValue(valueToNormalized(value));
}
}
/**
* Registers given listener callback to notify about changed selected values.
*
* @param listener The listener to notify about changed selected values.
*/
@SuppressWarnings("unused")
public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener<T> listener) {
this.listener = listener;
}
/**
* Set the path that defines the shadow of the thumb. This path should be defined assuming
* that the center of the shadow is at the top left corner (0,0) of the canvas. The
* {@link #drawThumbShadow(float, Canvas)} method will place the shadow appropriately.
*
* @param thumbShadowPath The path defining the thumb shadow
*/
@SuppressWarnings("unused")
public void setThumbShadowPath(Path thumbShadowPath) {
this.mThumbShadowPath = thumbShadowPath;
}
/**
* Handles thumb selection and movement. Notifies listener callback on certain events.
*/
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (!isEnabled()) {
return false;
}
int pointerIndex;
final int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// Remember where the motion event started
mActivePointerId = event.getPointerId(event.getPointerCount() - 1);
pointerIndex = event.findPointerIndex(mActivePointerId);
mDownMotionX = event.getX(pointerIndex);
pressedThumb = evalPressedThumb(mDownMotionX);
// Only handle thumb presses.
if (pressedThumb == null) {
return super.onTouchEvent(event);
}
setPressed(true);
invalidate();
onStartTrackingTouch();
trackTouchEvent(event);
attemptClaimDrag();
break;
case MotionEvent.ACTION_MOVE:
if (pressedThumb != null) {
if (mIsDragging) {
trackTouchEvent(event);
} else {
// Scroll to follow the motion event
pointerIndex = event.findPointerIndex(mActivePointerId);
final float x = event.getX(pointerIndex);
if (Math.abs(x - mDownMotionX) > mScaledTouchSlop) {
setPressed(true);
invalidate();
onStartTrackingTouch();
trackTouchEvent(event);
attemptClaimDrag();
}
}
if (notifyWhileDragging && listener != null) {
listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue());
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsDragging) {
trackTouchEvent(event);
onStopTrackingTouch();
setPressed(false);
} else {
// Touch up when we never crossed the touch slop threshold
// should be interpreted as a tap-seek to that location.
onStartTrackingTouch();
trackTouchEvent(event);
onStopTrackingTouch();
}
pressedThumb = null;
invalidate();
if (listener != null) {
listener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue());
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = event.getPointerCount() - 1;
// final int index = ev.getActionIndex();
mDownMotionX = event.getX(index);
mActivePointerId = event.getPointerId(index);
invalidate();
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(event);
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
onStopTrackingTouch();
setPressed(false);
}
invalidate(); // see above explanation
break;
}
return true;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose
// a new active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mDownMotionX = ev.getX(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
private void trackTouchEvent(MotionEvent event) {
final int pointerIndex = event.findPointerIndex(mActivePointerId);
final float x = event.getX(pointerIndex);
if (Thumb.MIN.equals(pressedThumb) && !mSingleThumb) {
setNormalizedMinValue(screenToNormalized(x));
} else if (Thumb.MAX.equals(pressedThumb)) {
setNormalizedMaxValue(screenToNormalized(x));
}
}
/**
* Tries to claim the user's drag motion, and requests disallowing any ancestors from stealing events in the drag.
*/
private void attemptClaimDrag() {
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
/**
* This is called when the user has started touching this widget.
*/
void onStartTrackingTouch() {
mIsDragging = true;
}
/**
* This is called when the user either releases his touch or the touch is canceled.
*/
void onStopTrackingTouch() {
mIsDragging = false;
}
/**
* Ensures correct size of the widget.
*/
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 200;
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
width = MeasureSpec.getSize(widthMeasureSpec);
}
int height = thumbImage.getHeight()
+ (!mShowTextAboveThumbs ? 0 : Utils.dpToPx(getContext(), HEIGHT_IN_DP))
+ (mThumbShadow ? mThumbShadowYOffset + mThumbShadowBlur : 0);
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
}
setMeasuredDimension(width, height);
}
/**
* Draws the widget on the given canvas.
*/
@Override
protected synchronized void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
paint.setTextSize(mTextSize);
paint.setStyle(Style.FILL);
paint.setColor(mDefaultColor);
paint.setAntiAlias(true);
float minMaxLabelSize = 0;
if (mShowLabels) {
// draw min and max labels
String minLabel = getContext().getString(R.string.Min);
String maxLabel = getContext().getString(R.string.Max);
minMaxLabelSize = Math.max(paint.measureText(minLabel), paint.measureText(maxLabel));
float minMaxHeight = mTextOffset + mThumbHalfHeight + mTextSize / 3;
canvas.drawText(minLabel, 0, minMaxHeight, paint);
canvas.drawText(maxLabel, getWidth() - minMaxLabelSize, minMaxHeight, paint);
}
padding = mInternalPad + minMaxLabelSize + mThumbHalfWidth;
// draw seek bar background line
mRect.left = padding;
mRect.right = getWidth() - padding;
canvas.drawRect(mRect, paint);
boolean selectedValuesAreDefault = getSelectedMinValue().equals(getAbsoluteMinValue()) &&
getSelectedMaxValue().equals(getAbsoluteMaxValue());
int colorToUseForButtonsAndHighlightedLine = !mAlwaysActive && !mActivateOnDefaultValues && selectedValuesAreDefault ?
mDefaultColor : // default values
mActiveColor; // non default, filter is active
// draw seek bar active range line
mRect.left = normalizedToScreen(normalizedMinValue);
mRect.right = normalizedToScreen(normalizedMaxValue);
paint.setColor(colorToUseForButtonsAndHighlightedLine);
canvas.drawRect(mRect, paint);
// draw minimum thumb (& shadow if requested) if not a single thumb control
if (!mSingleThumb) {
if (mThumbShadow) {
drawThumbShadow(normalizedToScreen(normalizedMinValue), canvas);
}
drawThumb(normalizedToScreen(normalizedMinValue), Thumb.MIN.equals(pressedThumb), canvas,
selectedValuesAreDefault);
}
// draw maximum thumb & shadow (if necessary)
if (mThumbShadow) {
drawThumbShadow(normalizedToScreen(normalizedMaxValue), canvas);
}
drawThumb(normalizedToScreen(normalizedMaxValue), Thumb.MAX.equals(pressedThumb), canvas,
selectedValuesAreDefault);
// draw the text if sliders have moved from default edges
if (mShowTextAboveThumbs && (mActivateOnDefaultValues || !selectedValuesAreDefault)) {
paint.setTextSize(mTextSize);
paint.setColor(mTextAboveThumbsColor);
// give text a bit more space here so it doesn't get cut off
int offset = Utils.dpToPx(getContext(), TEXT_LATERAL_PADDING_IN_DP);
String minText = String.valueOf(getSelectedMinValue());
String maxText = String.valueOf(getSelectedMaxValue());
float minTextWidth = paint.measureText(minText) + offset;
float maxTextWidth = paint.measureText(maxText) + offset;
if (!mSingleThumb) {
canvas.drawText(minText,
normalizedToScreen(normalizedMinValue) - minTextWidth * 0.5f,
mDistanceToTop + mTextSize,
paint);
}
canvas.drawText(maxText,
normalizedToScreen(normalizedMaxValue) - maxTextWidth * 0.5f,
mDistanceToTop + mTextSize,
paint);
}
}
/**
* Overridden to save instance state when device orientation changes. This method is called automatically if you assign an id to the RangeSeekBar widget using the {@link #setId(int)} method. Other members of this class than the normalized min and max values don't need to be saved.
*/
@Override
protected Parcelable onSaveInstanceState() {
final Bundle bundle = new Bundle();
bundle.putParcelable("SUPER", super.onSaveInstanceState());
bundle.putDouble("MIN", normalizedMinValue);
bundle.putDouble("MAX", normalizedMaxValue);
return bundle;
}
/**
* Overridden to restore instance state when device orientation changes. This method is called automatically if you assign an id to the RangeSeekBar widget using the {@link #setId(int)} method.
*/
@Override
protected void onRestoreInstanceState(Parcelable parcel) {
final Bundle bundle = (Bundle) parcel;
super.onRestoreInstanceState(bundle.getParcelable("SUPER"));
normalizedMinValue = bundle.getDouble("MIN");
normalizedMaxValue = bundle.getDouble("MAX");
}
/**
* Draws the "normal" resp. "pressed" thumb image on specified x-coordinate.
*
* @param screenCoord The x-coordinate in screen space where to draw the image.
* @param pressed Is the thumb currently in "pressed" state?
* @param canvas The canvas to draw upon.
*/
private void drawThumb(float screenCoord, boolean pressed, Canvas canvas, boolean areSelectedValuesDefault) {
Bitmap buttonToDraw;
if (!mActivateOnDefaultValues && areSelectedValuesDefault) {
buttonToDraw = thumbDisabledImage;
} else {
buttonToDraw = pressed ? thumbPressedImage : thumbImage;
}
canvas.drawBitmap(buttonToDraw, screenCoord - mThumbHalfWidth,
mTextOffset,
paint);
}
/**
* Draws a drop shadow beneath the slider thumb.
*
* @param screenCoord the x-coordinate of the slider thumb
* @param canvas the canvas on which to draw the shadow
*/
private void drawThumbShadow(float screenCoord, Canvas canvas) {
mThumbShadowMatrix.setTranslate(screenCoord + mThumbShadowXOffset, mTextOffset + mThumbHalfHeight + mThumbShadowYOffset);
mTranslatedThumbShadowPath.set(mThumbShadowPath);
mTranslatedThumbShadowPath.transform(mThumbShadowMatrix);
canvas.drawPath(mTranslatedThumbShadowPath, shadowPaint);
}
/**
* Decides which (if any) thumb is touched by the given x-coordinate.
*
* @param touchX The x-coordinate of a touch event in screen space.
* @return The pressed thumb or null if none has been touched.
*/
private Thumb evalPressedThumb(float touchX) {
Thumb result = null;
boolean minThumbPressed = isInThumbRange(touchX, normalizedMinValue);
boolean maxThumbPressed = isInThumbRange(touchX, normalizedMaxValue);
if (minThumbPressed && maxThumbPressed) {
// if both thumbs are pressed (they lie on top of each other), choose the one with more room to drag. this avoids "stalling" the thumbs in a corner, not being able to drag them apart anymore.
result = (touchX / getWidth() > 0.5f) ? Thumb.MIN : Thumb.MAX;
} else if (minThumbPressed) {
result = Thumb.MIN;
} else if (maxThumbPressed) {
result = Thumb.MAX;
}
return result;
}
/**
* Decides if given x-coordinate in screen space needs to be interpreted as "within" the normalized thumb x-coordinate.
*
* @param touchX The x-coordinate in screen space to check.
* @param normalizedThumbValue The normalized x-coordinate of the thumb to check.
* @return true if x-coordinate is in thumb range, false otherwise.
*/
private boolean isInThumbRange(float touchX, double normalizedThumbValue) {
return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= mThumbHalfWidth;
}
/**
* Sets normalized min value to value so that 0 <= value <= normalized max value <= 1. The View will get invalidated when calling this method.
*
* @param value The new normalized min value to set.
*/
private void setNormalizedMinValue(double value) {
normalizedMinValue = Math.max(0d, Math.min(1d, Math.min(value, normalizedMaxValue)));
invalidate();
}
/**
* Sets normalized max value to value so that 0 <= normalized min value <= value <= 1. The View will get invalidated when calling this method.
*
* @param value The new normalized max value to set.
*/
private void setNormalizedMaxValue(double value) {
normalizedMaxValue = Math.max(0d, Math.min(1d, Math.max(value, normalizedMinValue)));
invalidate();
}
/**
* Converts a normalized value to a Number object in the value space between absolute minimum and maximum.
*/
@SuppressWarnings("unchecked")
private T normalizedToValue(double normalized) {
double v = absoluteMinValuePrim + normalized * (absoluteMaxValuePrim - absoluteMinValuePrim);
// TODO parameterize this rounding to allow variable decimal points
return (T) numberType.toNumber(Math.round(v * 100) / 100d);
}
/**
* Converts the given Number value to a normalized double.
*
* @param value The Number value to normalize.
* @return The normalized double.
*/
private double valueToNormalized(T value) {
if (0 == absoluteMaxValuePrim - absoluteMinValuePrim) {
// prevent division by zero, simply return 0.
return 0d;
}
return (value.doubleValue() - absoluteMinValuePrim) / (absoluteMaxValuePrim - absoluteMinValuePrim);
}
/**
* Converts a normalized value into screen space.
*
* @param normalizedCoord The normalized value to convert.
* @return The converted value in screen space.
*/
private float normalizedToScreen(double normalizedCoord) {
return (float) (padding + normalizedCoord * (getWidth() - 2 * padding));
}
/**
* Converts screen space x-coordinates into normalized values.
*
* @param screenCoord The x-coordinate in screen space to convert.
* @return The normalized value.
*/
private double screenToNormalized(float screenCoord) {
int width = getWidth();
if (width <= 2 * padding) {
// prevent division by zero, simply return 0.
return 0d;
} else {
double result = (screenCoord - padding) / (width - 2 * padding);
return Math.min(1d, Math.max(0d, result));
}
}
/**
* Thumb constants (min and max).
*/
private enum Thumb {
MIN, MAX
}
/**
* Utility enumeration used to convert between Numbers and doubles.
*
* @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
*/
private enum NumberType {
LONG, DOUBLE, INTEGER, FLOAT, SHORT, BYTE, BIG_DECIMAL;
public static <E extends Number> NumberType fromNumber(E value) throws IllegalArgumentException {
if (value instanceof Long) {
return LONG;
}
if (value instanceof Double) {
return DOUBLE;
}
if (value instanceof Integer) {
return INTEGER;
}
if (value instanceof Float) {
return FLOAT;
}
if (value instanceof Short) {
return SHORT;
}
if (value instanceof Byte) {
return BYTE;
}
if (value instanceof BigDecimal) {
return BIG_DECIMAL;
}
throw new IllegalArgumentException("Number class '" + value.getClass().getName() + "' is not supported");
}
public Number toNumber(double value) {
switch (this) {
case LONG:
return (long) value;
case DOUBLE:
return value;
case INTEGER:
return (int) value;
case FLOAT:
return (float) value;
case SHORT:
return (short) value;
case BYTE:
return (byte) value;
case BIG_DECIMAL:
return BigDecimal.valueOf(value);
}
throw new InstantiationError("can't convert " + this + " to a Number object");
}
}
/**
* Callback listener interface to notify about changed range values.
*
* @param <T> The Number type the RangeSeekBar has been declared with.
* @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
*/
public interface OnRangeSeekBarChangeListener<T> {
void onRangeSeekBarValuesChanged(RangeSeekBar<?> bar, T minValue, T maxValue);
}
}