package com.ashideas.rnrangeslider; import android.content.Context; import android.graphics.Canvas; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import java.text.SimpleDateFormat; import java.util.Date; public class RangeSlider extends View { public enum LabelStyle { BUBBLE, NONE } public enum Gravity { TOP, BOTTOM, CENTER } private static final float SQRT_3 = (float) Math.sqrt(3); private static final float SQRT_3_2 = SQRT_3 / 2; private static final int THUMB_LOW = 0; private static final int THUMB_HIGH = 1; private static final int THUMB_NONE = -1; private OnValueChangeListener onValueChangeListener; private OnSliderTouchListener onSliderTouchListener; private Paint selectionPaint; private Paint blankPaint; private Paint thumbPaint; private Paint thumbBorderPaint; private Paint labelPaint; private Paint labelBorderPaint; private Paint labelTextPaint; private float thumbRadius; private float thumbBorderWidth; private LabelStyle labelStyle; private Path labelPath; private String textFormat; private float labelPadding; private float labelBorderWidth; private boolean rangeEnabled; private String valueType; private SimpleDateFormat dateTimeFormat; private Date dateTime; private Gravity gravity; private long minValue; private long maxValue; private long step; private boolean initialLowValueSet; private boolean initialHighValueSet; private long lowValue; private long highValue; private float labelTailHeight; private float labelGapHeight; private int activePointerId; private int activeThumb; public RangeSlider(Context context) { super(context); init(); } public RangeSlider(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public RangeSlider(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { dateTimeFormat = new SimpleDateFormat(); dateTime = new Date(); activePointerId = -1; activeThumb = THUMB_NONE; minValue = Long.MIN_VALUE; maxValue = Long.MAX_VALUE; lowValue = minValue; highValue = maxValue; step = 1; labelPath = new Path(); selectionPaint = new Paint(Paint.ANTI_ALIAS_FLAG); selectionPaint.setStrokeCap(Paint.Cap.ROUND); blankPaint = new Paint(Paint.ANTI_ALIAS_FLAG); blankPaint.setStrokeCap(Paint.Cap.ROUND); labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); labelPaint.setStyle(Paint.Style.FILL); labelBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); labelBorderPaint.setStyle(Paint.Style.FILL); labelTextPaint = new Paint(); thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG); thumbBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); } public void setOnValueChangeListener(OnValueChangeListener onValueChangeListener) { this.onValueChangeListener = onValueChangeListener; } public void setOnSliderTouchListener(OnSliderTouchListener onSliderTouchListener) { this.onSliderTouchListener = onSliderTouchListener; } public void setLineWidth(float lineWidth) { lineWidth = dpToPx(lineWidth); selectionPaint.setStrokeWidth(lineWidth); blankPaint.setStrokeWidth(lineWidth); ViewCompat.postInvalidateOnAnimation(this); } public void setThumbRadius(float thumbRadius) { this.thumbRadius = dpToPx(thumbRadius); ViewCompat.postInvalidateOnAnimation(this); } public void setThumbBorderWidth(float thumbBorderWidth) { this.thumbBorderWidth = dpToPx(thumbBorderWidth); thumbPaint.setStrokeWidth(dpToPx(this.thumbBorderWidth)); ViewCompat.postInvalidateOnAnimation(this); } public void setTextSize(float textSize) { labelTextPaint.setTextSize(dpToPx(textSize)); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelBorderWidth(float labelBorderWidth) { this.labelBorderWidth = dpToPx(labelBorderWidth); labelPaint.setStrokeWidth(this.labelBorderWidth); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelPadding(float labelPadding) { this.labelPadding = dpToPx(labelPadding); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelBorderRadius(float labelBorderRadius) { labelBorderRadius = dpToPx(labelBorderRadius); if (labelBorderRadius < 0) { labelBorderRadius = 0; } labelBorderPaint.setPathEffect(new CornerPathEffect(labelBorderRadius)); labelPaint.setPathEffect(new CornerPathEffect(labelBorderRadius)); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelTailHeight(float labelTailHeight) { this.labelTailHeight = dpToPx(labelTailHeight); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelGapHeight(float labelGapHeight) { this.labelGapHeight = dpToPx(labelGapHeight); ViewCompat.postInvalidateOnAnimation(this); } public void setTextFormat(String textFormat) { this.textFormat = textFormat; if ("time".equals(valueType)) { dateTimeFormat.applyPattern(textFormat == null ? "" : textFormat); } ViewCompat.postInvalidateOnAnimation(this); } public void setLabelStyle(String labelStyle) { this.labelStyle = labelStyle == null ? LabelStyle.BUBBLE : LabelStyle.valueOf(labelStyle.toUpperCase()); ViewCompat.postInvalidateOnAnimation(this); } public void setRangeEnabled(boolean rangeEnabled) { this.rangeEnabled = rangeEnabled; if (rangeEnabled) { if (highValue < lowValue) { highValue = lowValue; } if (highValue > maxValue) { highValue = maxValue; } if (lowValue > highValue) { lowValue = highValue; } } ViewCompat.postInvalidateOnAnimation(this); } public void setValueType(String valueType) { this.valueType = valueType; if ("time".equals(valueType)) { dateTimeFormat.applyPattern(textFormat == null ? "" : textFormat); } ViewCompat.postInvalidateOnAnimation(this); } public void setGravity(String gravity) { this.gravity = gravity == null ? Gravity.TOP : Gravity.valueOf(gravity.toUpperCase()); ViewCompat.postInvalidateOnAnimation(this); } public void setSelectionColor(String color) { selectionPaint.setColor(Utils.parseRgba(color)); ViewCompat.postInvalidateOnAnimation(this); } public void setBlankColor(String color) { blankPaint.setColor(Utils.parseRgba(color)); ViewCompat.postInvalidateOnAnimation(this); } public void setThumbColor(String color) { thumbPaint.setColor(Utils.parseRgba(color)); ViewCompat.postInvalidateOnAnimation(this); } public void setThumbBorderColor(String color) { thumbBorderPaint.setColor(Utils.parseRgba(color)); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelBackgroundColor(String color) { labelPaint.setColor(Utils.parseRgba(color)); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelTextColor(String color) { labelTextPaint.setColor(Utils.parseRgba(color)); ViewCompat.postInvalidateOnAnimation(this); } public void setLabelBorderColor(String color) { labelBorderPaint.setColor(Utils.parseRgba(color)); ViewCompat.postInvalidateOnAnimation(this); } public void setMinValue(long minValue) { if (minValue <= maxValue) { this.minValue = minValue; fitToMinMax(); } ViewCompat.postInvalidateOnAnimation(this); } public void setMaxValue(long maxValue) { if (maxValue > minValue) { this.maxValue = maxValue; fitToMinMax(); } ViewCompat.postInvalidateOnAnimation(this); } private void fitToMinMax() { long oldLow = lowValue; long oldHigh = highValue; lowValue = Utils.clamp(lowValue, minValue, maxValue); highValue = Utils.clamp(highValue, minValue, maxValue); checkAndFireValueChangeEvent(oldLow, oldHigh, false); } public void setStep(long step) { this.step = step; } public void setInitialLowValue(long lowValue) { if (!initialLowValueSet) { initialLowValueSet = true; this.setLowValue(lowValue); } } /** * This method should never be called because of user's touch. * * @param lowValue */ public void setLowValue(long lowValue) { long oldLow = this.lowValue; this.lowValue = Utils.clamp(lowValue, minValue, rangeEnabled ? highValue : maxValue); checkAndFireValueChangeEvent(oldLow, highValue, false); ViewCompat.postInvalidateOnAnimation(this); } public void setInitialHighValue(long highValue) { if (!initialHighValueSet) { initialHighValueSet = true; this.setHighValue(highValue); } } /** * This method should never be called because of user's touch. * * @param highValue */ public void setHighValue(long highValue) { long oldHigh = this.highValue; this.highValue = Utils.clamp(highValue, lowValue, maxValue); checkAndFireValueChangeEvent(lowValue, oldHigh, false); ViewCompat.postInvalidateOnAnimation(this); } @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) { return false; } int actionIndex = event.getActionIndex(); long oldLow = this.lowValue; long oldHigh = this.highValue; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: activePointerId = event.getPointerId(actionIndex); handleTouchDown(getValueForPosition(event.getX())); if (onSliderTouchListener != null) { onSliderTouchListener.onTouchStart(); } break; case MotionEvent.ACTION_MOVE: long pointerValue = getValueForPosition(event.getX(event.findPointerIndex(activePointerId))); handleTouchMove(pointerValue); break; case MotionEvent.ACTION_UP: activePointerId = -1; activeThumb = THUMB_NONE; if (onSliderTouchListener != null) { onSliderTouchListener.onTouchEnd(); } break; } ViewCompat.postInvalidateOnAnimation(this); checkAndFireValueChangeEvent(oldLow, oldHigh, true); return true; } private void checkAndFireValueChangeEvent(long oldLow, long oldHigh, boolean fromUser) { if (onValueChangeListener == null || (oldLow == lowValue && oldHigh == highValue) || minValue == Long.MIN_VALUE || maxValue == Long.MAX_VALUE) { return; } onValueChangeListener.onValueChanged(lowValue, highValue, fromUser); } private void handleTouchDown(long pointerValue) { if ( !rangeEnabled || (lowValue == highValue && pointerValue < lowValue) || Math.abs(pointerValue - lowValue) < Math.abs(pointerValue - highValue) // The closer thumb ) { activeThumb = THUMB_LOW; lowValue = pointerValue; } else { activeThumb = THUMB_HIGH; highValue = pointerValue; } } private void handleTouchMove(long pointerValue) { if (!rangeEnabled) { lowValue = pointerValue; } else if (activeThumb == THUMB_LOW) { lowValue = Utils.clamp(pointerValue, minValue, highValue); } else if (activeThumb == THUMB_HIGH) { highValue = Utils.clamp(pointerValue, lowValue, maxValue); } } private long getValueForPosition(float position) { if (position <= thumbRadius) { return minValue; } else if (position >= getWidth() - thumbRadius) { return maxValue; } else { double availableWidth = getWidth() - 2 * thumbRadius; position -= thumbRadius; long relativePosition = (long) ((maxValue - minValue) * position / availableWidth); return minValue + relativePosition - relativePosition % step; } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (minValue == Long.MIN_VALUE || maxValue == Long.MAX_VALUE) { //Values are not set yet, don't draw anything return; } float labelTextHeight = getLabelTextHeight(); float labelHeight = labelStyle == LabelStyle.NONE ? 0 : 2 * labelBorderWidth + labelTailHeight + labelTextHeight + 2 * labelPadding; float labelAndGapHeight = labelStyle == LabelStyle.NONE ? 0 : labelHeight + labelGapHeight; float drawingHeight = labelAndGapHeight + 2 * thumbRadius; float height = getHeight(); if (height > drawingHeight) { if (gravity == Gravity.BOTTOM) { canvas.translate(0, height - drawingHeight); } else if (gravity == Gravity.CENTER) { canvas.translate(0, (height - drawingHeight) / 2); } } float cy = labelAndGapHeight + thumbRadius; float width = getWidth(); float availableWidth = width - 2 * thumbRadius; // Draw the blank line canvas.drawLine(thumbRadius, cy, width - thumbRadius, cy, blankPaint); float lowX = thumbRadius + availableWidth * (lowValue - minValue) / (maxValue - minValue); float highX = thumbRadius + availableWidth * (highValue - minValue) / (maxValue - minValue); // Draw the selected line if (rangeEnabled) { canvas.drawLine(lowX, cy, highX, cy, selectionPaint); } else { canvas.drawLine(thumbRadius, cy, lowX, cy, selectionPaint); } if (thumbRadius > 0) { drawThumb(canvas, lowX, cy); if (rangeEnabled) { drawThumb(canvas, highX, cy); } } if (labelStyle == LabelStyle.NONE || activeThumb == THUMB_NONE) { return; } String text = formatLabelText(activeThumb == THUMB_LOW ? lowValue : highValue); float labelTextWidth = labelTextPaint.measureText(text); float labelWidth = labelTextWidth + 2 * labelPadding + 2 * labelBorderWidth; float cx = activeThumb == THUMB_LOW ? lowX : highX; if (labelWidth < labelTailHeight / SQRT_3_2) { labelWidth = labelTailHeight / SQRT_3_2; } float y = labelHeight; // Bounds of outer rectangular part float top = 0; float left = cx - labelWidth / 2; float right = left + labelWidth; float bottom = top + labelHeight - labelTailHeight; float overflowOffset = 0; if (left < 0) { overflowOffset = -left; } else if (right > width) { overflowOffset = width - right; } left += overflowOffset; right += overflowOffset; preparePath(cx, y, left, top, right, bottom, labelTailHeight); canvas.drawPath(labelPath, labelBorderPaint); labelPath.reset(); y = 2 * labelPadding + labelTextHeight + labelTailHeight; // Bounds of inner rectangular part top = labelBorderWidth; left = cx - labelTextWidth / 2 - labelPadding + overflowOffset; right = left + labelTextWidth + 2 * labelPadding; bottom = labelBorderWidth + 2 * labelPadding + labelTextHeight; preparePath(cx, y, left, top, right, bottom, labelTailHeight - labelBorderWidth); canvas.drawPath(labelPath, labelPaint); canvas.drawText(text, cx - labelTextWidth / 2 + overflowOffset, labelBorderWidth + labelPadding - labelTextPaint.ascent(), labelTextPaint); } private void drawThumb(Canvas canvas, float x, float y) { canvas.drawCircle(x, y, thumbRadius, thumbBorderPaint); canvas.drawCircle(x, y, thumbRadius - thumbBorderWidth, thumbPaint); } private void preparePath(float x, float y, float left, float top, float right, float bottom, float tailHeight) { float cx = x; labelPath.reset(); labelPath.moveTo(x, y); x = cx + tailHeight / SQRT_3; y = bottom; labelPath.lineTo(x, y); x = right; labelPath.lineTo(x, y); y = top; labelPath.lineTo(x, y); x = left; labelPath.lineTo(x, y); y = bottom; labelPath.lineTo(x, y); x = cx - tailHeight / SQRT_3; labelPath.lineTo(x, y); labelPath.close(); } private float dpToPx(float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } private float getLabelTextHeight() { return labelTextPaint.descent() - labelTextPaint.ascent(); } /** * This method formats label text for selected value. * Change this method if you need more complex formatting. * * @param value * @return formatted text */ private String formatLabelText(long value) { if ("number".equals(valueType)) { return String.format(textFormat, value); } else if ("time".equals(valueType)) { dateTime.setTime(value); return dateTimeFormat.format(dateTime); } else { // For other formatting methods, add cases here return ""; } } public interface OnValueChangeListener { void onValueChanged(long lowValue, long highValue, boolean fromUser); } public interface OnSliderTouchListener { void onTouchStart(); void onTouchEnd(); } }