sjrudani / range-seek-bar

Automatically exported from code.google.com/p/range-seek-bar
0 stars 0 forks source link

Vertical implementation #22

Open GoogleCodeExporter opened 9 years ago

GoogleCodeExporter commented 9 years ago
Hi,

I changed the code to provide a vertical implementation. Maybe it is helpful 
for someone:

import java.math.BigDecimal;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ImageView;
import de.continental.app.gesturedetection.R;

/**
 * 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 MotionEvent} handling for smoother use, anti-aliased painting for improved aesthetics.
 * 
 * @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
 * @author Peter Sinnott (psinnott@gmail.com)
 * @author Thomas Barrasso (tbarrasso@sevenplusandroid.org)
 * 
 * @param <T>
 *            The Number type of the range values. One of Long, Double, Integer, Float, Short, Byte or BigDecimal.
 */
public class RangeSeekBar<T extends Number> extends ImageView {
        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 thumbPressedImage = BitmapFactory.decodeResource(getResources(), R.drawable.seek_thumb_pressed);
        private final float thumbWidth = thumbImage.getWidth();
        private final float thumbHalfWidth = 0.5f * thumbWidth;
        private final float thumbHalfHeight = 0.5f * thumbImage.getHeight();
        private final float lineWidth = 0.3f * thumbHalfWidth;
        private final float padding = thumbHalfHeight;
        private final T absoluteMinValue, absoluteMaxValue;
        private final NumberType numberType;
        private final double absoluteMinValuePrim, absoluteMaxValuePrim;
        private double normalizedMinValue = 0d;
        private double normalizedMaxValue = 1d;
        private Thumb pressedThumb = null;
        private boolean notifyWhileDragging = false;
        private OnRangeSeekBarChangeListener<T> listener;

        /**
         * Default color of a {@link RangeSeekBar}, #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 float mDownMotionY;
        private int mActivePointerId = INVALID_POINTER_ID;

        /**
         * On touch, this offset plus the scaled value from the position of the touch will form the progress value. Usually 0.
         */
        float mTouchProgressOffset;

        private int mScaledTouchSlop;
        private boolean mIsDragging;

        /**
         * Creates a new RangeSeekBar.
         * 
         * @param absoluteMinValue
         *            The minimum value of the selectable range.
         * @param absoluteMaxValue
         *            The maximum value of the selectable range.
         * @param context
         * @throws IllegalArgumentException
         *             Will be thrown if min/max value type is not one of Long, Double, Integer, Float, Short, Byte or BigDecimal.
         */
        public RangeSeekBar(T absoluteMinValue, T absoluteMaxValue, Context context) throws IllegalArgumentException {
                super(context);
                this.absoluteMinValue = absoluteMinValue;
                this.absoluteMaxValue = absoluteMaxValue;
                absoluteMinValuePrim = absoluteMinValue.doubleValue();
                absoluteMaxValuePrim = absoluteMaxValue.doubleValue();
                numberType = NumberType.fromNumber(absoluteMinValue);

                // 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);
                init();
        }

        private final void init() {
                mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        }

        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 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.
         */
        public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener<T> listener) {
                this.listener = listener;
        }

        /**
         * Handles thumb selection and movement. Notifies listener callback on certain events.
         */
        @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)) {
                        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 height = 200;
            if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
                    height = MeasureSpec.getSize(heightMeasureSpec);
            }
            int width = thumbImage.getWidth();
            if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
                    width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec));
            }
            setMeasuredDimension(width, height);

        }

        /**
         * Draws the widget on the given canvas.
         */
        @Override
        protected synchronized void onDraw(Canvas canvas) {
                super.onDraw(canvas);

                // draw seek bar background line
                final RectF rect = new RectF(0.5f * (getWidth() - lineWidth)
                        , padding
                        , 0.5f * (getWidth() + lineWidth)
                        , getHeight() - padding);
                paint.setStyle(Style.FILL);
                paint.setColor(Color.GRAY);
                paint.setAntiAlias(true);
                canvas.drawRect(rect, paint);

                // draw seek bar active range line
                rect.top = normalizedToScreen(normalizedMinValue);
                rect.bottom = normalizedToScreen(normalizedMaxValue);

                // orange color
                paint.setColor(DEFAULT_COLOR);
                canvas.drawRect(rect, paint);

                // draw minimum thumb
                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) {
                canvas.drawBitmap(pressed ? thumbPressedImage : thumbImage
                        ,(float) ((0.5f * getWidth()) - thumbHalfWidth) 
                        , screenCoord - thumbHalfHeight
                        , paint);
        }

        /**
         * Decides which (if any) thumb is touched by the given x-coordinate.
         * 
         * @param touchY
         *            The y-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 y-coordinate in screen space to check.
         * @param normalizedThumbValue
         *            The normalized x-coordinate of the thumb to check.
         * @return true if y-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.
         */
        public 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.
         */
        public 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 T normalizedToValue(double normalized) {
                return (T) numberType.toNumber(absoluteMinValuePrim + normalized * (absoluteMaxValuePrim - absoluteMinValuePrim));
        }

        /**
         * 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 * (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));
                }
        }

        /**
         * Callback listener interface to notify about changed range values.
         * 
         * @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
         * 
         * @param <T>
         *            The Number type the RangeSeekBar has been declared with.
         */
        public interface OnRangeSeekBarChangeListener<T> {
                public void onRangeSeekBarValuesChanged(RangeSeekBar<?> bar, T minValue, T maxValue);
        }

        /**
         * Thumb constants (min and max).
         */
        private static enum Thumb {
                MIN, MAX
        };

        /**
         * Utility enumaration used to convert between Numbers and doubles.
         * 
         * @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
         * 
         */
        private static 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 new Long((long) value);
                        case DOUBLE:
                                return value;
                        case INTEGER:
                                return new Integer((int) value);
                        case FLOAT:
                                return new Float(value);
                        case SHORT:
                                return new Short((short) value);
                        case BYTE:
                                return new Byte((byte) value);
                        case BIG_DECIMAL:
                                return new BigDecimal(value);
                        }
                        throw new InstantiationError("can't convert " + this + " to a Number object");
                }
        }
}

Original issue reported on code.google.com by plin...@gmail.com on 2 Apr 2013 at 1:41

GoogleCodeExporter commented 9 years ago
Thanks for submitting this. It would be even cooler, if orientation were 
configurable with a setter, right?

Original comment by tittel@kom.e-technik.tu-darmstadt.de on 3 Apr 2013 at 6:24

GoogleCodeExporter commented 9 years ago
Of course, I definitely agree. But currently I only need a vertical 
implementation. :)

Original comment by plin...@gmail.com on 3 Apr 2013 at 7:49

GoogleCodeExporter commented 9 years ago
I would love to implement this but what are these files referring to?
R.drawable.seek_thumb_normal and .seek_thumb_pressed

Thank you
Sara

Original comment by sarav1...@gmail.com on 19 Apr 2013 at 3:10

GoogleCodeExporter commented 9 years ago
sarav1234, please read the introduction to the widget. The files refer to the 
thumb images you need to download.

Original comment by tittel@kom.e-technik.tu-darmstadt.de on 19 Apr 2013 at 6:16

GoogleCodeExporter commented 9 years ago
thank you so much, you saved me a lot of time I guess.

Original comment by alessand...@gmail.com on 29 Oct 2013 at 9:49

GoogleCodeExporter commented 9 years ago
Hi Guys,

Its a great widget. Not sure if its still an active group, but for anyone who 
stumbles across ... Here is a generic implementation to control orientation at 
init.

In the constructor for RangeSeekBar, define the Orientation as well before use. 
Orientation.Vertical or Orientation.Horizontal.

Cheers
Optimus

Original comment by dev.TheM...@gmail.com on 28 Aug 2014 at 6:56

Attachments: