Last active
March 14, 2019 05:45
-
-
Save piyush-malaviya/b9ee35d890661acfed61f4b3935d8256 to your computer and use it in GitHub Desktop.
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="VerticalRangeSeekBar"> | |
<attr name="absoluteMinValue" format="integer|float"/> | |
<attr name="absoluteMaxValue" format="integer|float"/> | |
<attr name="singleThumb" format="boolean"/> | |
</declare-styleable> | |
</resources> |
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
/** | |
* Util class for converting between dp, px and other magical pixel units | |
*/ | |
public class PixelUtil { | |
private PixelUtil() { | |
} | |
public static int dpToPx(Context context, int dp) { | |
int px = Math.round(dp * getPixelScaleFactor(context)); | |
return px; | |
} | |
public static int pxToDp(Context context, int px) { | |
int dp = Math.round(px / getPixelScaleFactor(context)); | |
return dp; | |
} | |
private static float getPixelScaleFactor(Context context) { | |
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); | |
return (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT); | |
} | |
} |
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
public class VerticalRangeSeekBar extends AppCompatImageView { | |
public static final Integer DEFAULT_MINIMUM = 0; | |
public static final Integer DEFAULT_MAXIMUM = 100; | |
public static final int WIDTH_IN_DP = 30; | |
public static final int TEXT_LATERAL_PADDING_IN_DP = 3; | |
/** | |
* Default color of a {@link VerticalRangeSeekBar}, #FF33B5E5. This is also known as "Ice Cream Sandwich" blue. | |
*/ | |
public static final int DEFAULT_COLOR = Color.argb(0xFF, 0x33, 0xB5, 0xE5); | |
/** | |
* 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_UP = 0x6, ACTION_POINTER_INDEX_MASK = 0x0000ff00, ACTION_POINTER_INDEX_SHIFT = 8; | |
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 final int LINE_WIDTH_IN_DP = 2; | |
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
//private final Bitmap thumbImage = BitmapFactory.decodeResource(getResources(), R.drawable.seek_thumb_normal); | |
private final Bitmap thumbImage = getBitmap(R.drawable.drawable_rounded_accent_thumb); | |
private final Bitmap thumbPressedImage = getBitmap(R.drawable.seek_thumb_pressed); | |
private final Bitmap thumbDisabledImage = getBitmap(R.drawable.seek_thumb_disabled); | |
private final float thumbWidth = thumbImage.getWidth(); | |
private final float thumbHalfWidth = 0.5f * thumbWidth; | |
private final float thumbHalfHeight = 0.5f * thumbImage.getHeight(); | |
private float INITIAL_PADDING; | |
private float padding; | |
private int 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 listener; | |
private float mDownMotionY; | |
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; | |
public VerticalRangeSeekBar(Context context) { | |
super(context); | |
init(context, null); | |
} | |
public VerticalRangeSeekBar(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
init(context, attrs); | |
} | |
public VerticalRangeSeekBar(Context context, AttributeSet attrs, int defStyle) { | |
super(context, attrs, defStyle); | |
init(context, attrs); | |
} | |
private int extractNumericValueFromAttributes(TypedArray a, int attribute, int defaultValue) { | |
TypedValue tv = a.peekValue(attribute); | |
if (tv == null) { | |
return defaultValue; | |
} | |
return a.getInteger(attribute, defaultValue); | |
} | |
private void init(Context context, AttributeSet attrs) { | |
if (attrs == null) { | |
setRangeToDefaultValues(); | |
} else { | |
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.VerticalRangeSeekBar, 0, 0); | |
setRangeValues( | |
extractNumericValueFromAttributes(a, R.styleable.VerticalRangeSeekBar_absoluteMinValue, DEFAULT_MINIMUM), | |
extractNumericValueFromAttributes(a, R.styleable.VerticalRangeSeekBar_absoluteMaxValue, DEFAULT_MAXIMUM)); | |
mSingleThumb = a.getBoolean(R.styleable.VerticalRangeSeekBar_singleThumb, false); | |
a.recycle(); | |
} | |
//setValuePrimAndNumberType(); | |
INITIAL_PADDING = PixelUtil.dpToPx(context, INITIAL_PADDING_IN_DP); | |
mTextSize = PixelUtil.dpToPx(context, DEFAULT_TEXT_SIZE_IN_DP); | |
mDistanceToTop = PixelUtil.dpToPx(context, DEFAULT_TEXT_DISTANCE_TO_TOP_IN_DP); | |
mTextOffset = this.mTextSize + PixelUtil.dpToPx(context, | |
DEFAULT_TEXT_DISTANCE_TO_BUTTON_IN_DP) + this.mDistanceToTop; | |
float lineWidth = PixelUtil.dpToPx(context, LINE_WIDTH_IN_DP); | |
mRect = new RectF( | |
mTextOffset / 2 + thumbHalfHeight - lineWidth / 2, | |
padding, | |
mTextOffset / 2 + thumbHalfHeight + lineWidth / 2, | |
getWidth() - padding); | |
// make RangeSeekBar focusable. This solves focus handling issues in case EditText widgets are being used along with the RangeSeekBar within ScollViews. | |
setFocusable(true); | |
setFocusableInTouchMode(true); | |
mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); | |
} | |
private Bitmap getBitmap(int drawableRes) { | |
Drawable drawable = getResources().getDrawable(drawableRes); | |
Canvas canvas = new Canvas(); | |
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); | |
canvas.setBitmap(bitmap); | |
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); | |
drawable.draw(canvas); | |
return bitmap; | |
} | |
public void setRangeValues(int minValue, int maxValue) { | |
this.absoluteMinValue = minValue; | |
this.absoluteMaxValue = maxValue; | |
setValuePrimAndNumberType(); | |
} | |
@SuppressWarnings("unchecked") | |
// only used to set default values when initialised from XML without any values specified | |
private void setRangeToDefaultValues() { | |
this.absoluteMinValue = DEFAULT_MINIMUM; | |
this.absoluteMaxValue = DEFAULT_MAXIMUM; | |
setValuePrimAndNumberType(); | |
} | |
private void setValuePrimAndNumberType() { | |
absoluteMinValuePrim = absoluteMinValue; | |
absoluteMaxValuePrim = absoluteMaxValue; | |
} | |
public void resetSelectedValues() { | |
setSelectedMinValue(absoluteMinValue); | |
setSelectedMaxValue(absoluteMaxValue); | |
} | |
public boolean isNotifyWhileDragging() { | |
return notifyWhileDragging; | |
} | |
/** | |
* Should the widget notify the listener callback while the user is still dragging a thumb? Default is false. | |
* | |
* @param flag | |
*/ | |
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 int 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 int getAbsoluteMaxValue() { | |
return absoluteMaxValue; | |
} | |
/** | |
* Returns the currently selected min value. | |
* | |
* @return The currently selected min value. | |
*/ | |
public int 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(int 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 int 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(int 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. | |
*/ | |
public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener listener) { | |
this.listener = listener; | |
} | |
/** | |
* Handles thumb selection and movement. Notifies listener callback on certain events. | |
*/ | |
@SuppressLint("ClickableViewAccessibility") | |
@Override | |
public boolean onTouchEvent(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); | |
mDownMotionY = event.getY(pointerIndex); | |
pressedThumb = evalPressedThumb(mDownMotionY); | |
// 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 y = event.getY(pointerIndex); | |
if (Math.abs(y - mDownMotionY) > 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(); | |
mDownMotionY = event.getY(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 final 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. | |
// TODO: Make this decision more intelligent. | |
final int newPointerIndex = pointerIndex == 0 ? 1 : 0; | |
mDownMotionY = ev.getY(newPointerIndex); | |
mActivePointerId = ev.getPointerId(newPointerIndex); | |
} | |
} | |
private final void trackTouchEvent(MotionEvent event) { | |
final int pointerIndex = event.findPointerIndex(mActivePointerId); | |
final float y = event.getY(pointerIndex); | |
if (Thumb.MIN.equals(pressedThumb) && !mSingleThumb) { | |
setNormalizedMinValue(screenToNormalized(y)); | |
} else if (Thumb.MAX.equals(pressedThumb)) { | |
setNormalizedMaxValue(screenToNormalized(y)); | |
} | |
} | |
/** | |
* 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 = thumbImage.getWidth() + PixelUtil.dpToPx(getContext(), WIDTH_IN_DP); | |
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) { | |
width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec)); | |
} | |
int height = 200; | |
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) { | |
height = MeasureSpec.getSize(heightMeasureSpec); | |
} | |
setMeasuredDimension(width, height); | |
} | |
/** | |
* Draws the widget on the given canvas. | |
*/ | |
@Override | |
protected synchronized void onDraw(Canvas canvas) { | |
super.onDraw(canvas); | |
paint.setTextSize(mTextSize); | |
paint.setStyle(Style.FILL); | |
paint.setColor(Color.GRAY); | |
paint.setAntiAlias(true); | |
// draw min and max labels | |
padding = INITIAL_PADDING + thumbHalfHeight; | |
// draw seek bar background line | |
mRect.top = padding; | |
mRect.bottom = getHeight() - padding; | |
canvas.drawRect(mRect, paint); | |
// draw seek bar active range line | |
mRect.top = normalizedToScreen(normalizedMinValue); | |
mRect.bottom = normalizedToScreen(normalizedMaxValue); | |
paint.setColor(DEFAULT_COLOR); | |
canvas.drawRect(mRect, paint); | |
// draw minimum thumb if not a single thumb control | |
if (!mSingleThumb) { | |
drawThumb(normalizedToScreen(normalizedMinValue), Thumb.MIN.equals(pressedThumb), canvas); | |
} | |
// draw maximum thumb | |
drawThumb(normalizedToScreen(normalizedMaxValue), Thumb.MAX.equals(pressedThumb), canvas); | |
} | |
/** | |
* 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) { | |
Bitmap buttonToDraw; | |
buttonToDraw = pressed ? thumbPressedImage : thumbImage; | |
canvas.drawBitmap(buttonToDraw, mTextOffset / 2, screenCoord - thumbHalfWidth, | |
paint); | |
} | |
/** | |
* Decides which (if any) thumb is touched by the given x-coordinate. | |
* | |
* @param touchY 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 touchY) { | |
Thumb result = null; | |
boolean minThumbPressed = isInThumbRange(touchY, normalizedMinValue); | |
boolean maxThumbPressed = isInThumbRange(touchY, 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 = (touchY / getHeight() > 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 touchY 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 touchY, double normalizedThumbValue) { | |
return Math.abs(touchY - normalizedToScreen(normalizedThumbValue)) <= thumbHalfHeight; | |
} | |
/** | |
* 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. | |
* | |
* @param normalized | |
* @return | |
*/ | |
@SuppressWarnings("unchecked") | |
private int normalizedToValue(double normalized) { | |
double v = absoluteMinValuePrim + normalized * (absoluteMaxValuePrim - absoluteMinValuePrim); | |
// TODO parameterize this rounding to allow variable decimal points | |
return (int) (Math.round(v * 100) / 100); | |
} | |
/** | |
* Converts the given Number value to a normalized double. | |
* | |
* @param value The Number value to normalize. | |
* @return The normalized double. | |
*/ | |
private double valueToNormalized(int value) { | |
if (0 == absoluteMaxValuePrim - absoluteMinValuePrim) { | |
// prevent division by zero, simply return 0. | |
return 0d; | |
} | |
return (value - 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 * (getHeight() - 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 height = getHeight(); | |
if (height <= 2 * padding) { | |
// prevent division by zero, simply return 0. | |
return 0d; | |
} else { | |
double result = (screenCoord - padding) / (height - 2 * padding); | |
return Math.min(1d, Math.max(0d, result)); | |
} | |
} | |
/** | |
* Thumb constants (min and max). | |
*/ | |
private static enum Thumb { | |
MIN, MAX | |
} | |
/** | |
* Callback listener interface to notify about changed range values. | |
* | |
* @author Stephan Tittel ([email protected]) | |
*/ | |
public interface OnRangeSeekBarChangeListener { | |
public void onRangeSeekBarValuesChanged(VerticalRangeSeekBar bar, int minValue, int maxValue); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment