com.dirkgassen.wator.ui.view.RangeSlider.java Source code

Java tutorial

Introduction

Here is the source code for com.dirkgassen.wator.ui.view.RangeSlider.java

Source

/*
 * RangeSlider.java is part of Wa-Tor (C) 2016 by Dirk Gassen.
 *
 * Wa-Tor is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Wa-Tor is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.dirkgassen.wator.ui.view;

import java.util.Arrays;
import java.util.Locale;

import com.dirkgassen.wator.R;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

/**
 * A "range slider" that allows the user to select an integer value by dragging a thumb. The slider can have one of
 * two modes:
 * <dl>
 *     <dt>Min to Max</dt>
 *     <dd>The user can drag across a range between a minimum value and a maximum value</dd>
 *
 *     <dt>A set of values</dt>
 *     <dd>The user can select from a set of (ordered) values</dd>
 * </ol>
 */
public class RangeSlider extends View {

    /**
     * Maximum for a click to have happened on the thumb. If the user touches the thumb and releases the finger in
     * this time frame without changing the value a click has happened and the user can enter the value in an alert.
     */
    private static final int MAX_CLICK_DURATION = 1000;

    /**  Interface to implement to listen for changes of the current value. */
    public interface OnValueChangeListener {

        /**
         * Called upon a change of the current value.
         *
         * @param slider The {@link RangeSlider} associated with this listener
         * @param oldVal The previous value
         * @param newVal The new value
         * @param fromUser {@code true} if the user initiated the change from the UI; {@code false} if the change
         *                             was done programmatically (e.g., by calling {@link #setValue(int)}})
         */
        void onValueChange(RangeSlider slider, int oldVal, int newVal, boolean fromUser);

    }

    /** Paint for the text inside the thumb */
    final private Paint thumbTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    /** Paint for the background of the thumb */
    final private Paint thumbBackgroundPaint = new Paint();

    /** Paint for the slider along which the thumb is dragged */
    final private Paint sliderPaint = new Paint();

    /** Path for the thumb */
    final Path thumbPath = new Path();

    /** Minimum selectable value (ignored if {@link #valueSet} is not {@code null}) */
    private int minValue;

    /** Maximum selectable value (ignored if {@link #valueSet} is not {@code null}) */
    private int maxValue;

    /** If this field is non-null then it is a sorted array of values that the user can pick from by sliding the thumb */
    private int[] valueSet;

    /** Current value of this slider */
    private int value;

    /** Padding between the outside of the thumb and the text of the thumb */
    private float thumbPadding;

    /** Formatting string for the thumb (the value should be represented by {@code %d}) */
    private String thumbFormat;

    private float thumbSize;
    /** (Precalculated) size of the thumb (width/height of the enclosing rectangle) */

    /** Density of the screen so that we don't always have to get the display metrics */
    private float displayDensity;

    /**
     * The {@link RangeSlider.OnValueChangeListener} that should be called upon if the value changes. Can be set with
     * {@link #setOnValueChangeListener(OnValueChangeListener)}).
     */
    private OnValueChangeListener onValueChangeListener;

    /**
     * When the user taps on the thumb and drags this field stores the offset between the original center of the
     * thumb and where the user tapped.
     */
    private float touchOffset;

    /**
     * When a touch occurs this field will be set to the index of the pointer of the touch. While dragging we need
     * to get the coordinates of that pointer for calculations.
     */
    private int activePointerId = -1;

    /** Stores the value before the user started dragging so that we can restore the value in case dragging is canceled */
    private int valueBeforeDragging;

    /** Flag that we are dragging (currently not really used, may be in the future) */
    private boolean isDragging = false;

    /**
     * Wall time of the start of a touch. This member will be reset if the thumb moves. It is being used to distinguish
     * between a "click" and dragging.
     */
    private long touchEventStartTime;

    /**
     * Changes the value, invalidates the view and calls upon the {@link RangeSlider.OnValueChangeListener} (if set).
     *
     * @param newValue new value to set
     * @param fromUser should be {@code true} if the new value is coming from the UI (user); otherwise {@code false}
     */
    synchronized private void updateValue(int newValue, boolean fromUser) {
        int oldValue = value;
        value = newValue;
        invalidate();
        if (onValueChangeListener != null) {
            onValueChangeListener.onValueChange(this, oldValue, newValue, fromUser);
        }
    }

    /**
     * Calculates the Euclidean distance between wo points
     *
     * @param x1 x coordinate of the first point
     * @param y1 y coordinate of the first point
     * @param x2 x coordinate of the second point
     * @param y2 y coordinate of the second point
     * @return euclidean distance between the two points
     */
    private float distance(float x1, float y1, float x2, float y2) {
        float dx = x1 - x2;
        float dy = y1 - y2;
        float distanceInPx = (float) Math.sqrt(dx * dx + dy * dy);
        return pxToDp(distanceInPx);
    }

    /**
     * Converts real pixels into density independent pixels.
     *
     * @param px pixels to convert
     * @return converted pixels
     */
    private float pxToDp(float px) {
        return px / displayDensity;
    }

    /**
     * Returns the position of the center of the thumb that correlates to the current {@link #value}.
     *
     * @param paddingLeft left padding of this slider
     * @param paddingRight right padding of this slider
     * @return position of the center of the thumb
     */
    private float positionFromValue(int paddingLeft, int paddingRight) {
        if (valueSet != null) {
            // Calculate for a value set: find the index of the value. We are finding the index of the first entry
            // which is larger than or equal to the value. The value _should_ always be an entry of the index, though.
            for (int no = 0; no < valueSet.length; no++) {
                if (value <= valueSet[no]) {
                    if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
                        Log.d("Wa-Tor", "Found current value " + value + " @ position " + no);
                    }
                    return (getWidth() - paddingLeft - paddingRight - thumbSize) * ((float) no)
                            / (valueSet.length - 1) + paddingLeft + thumbSize / 2;
                }
            }
            return paddingLeft + thumbSize / 2;
        }

        // Calculate for a linear range
        return (getWidth() - paddingLeft - paddingRight - thumbSize) * ((float) value - minValue)
                / (maxValue - minValue) + paddingLeft + thumbSize / 2;
    }

    /**
     * Calculates the value corresponding to the given position of a thumb
     * @param position position to calculate the value from
     * @param paddingLeft left padding of this slider
     * @param paddingRight right padding of this slider
     * @return value corresponding to the given position
     */
    private int valueFromPosition(float position, int paddingLeft, int paddingRight) {
        if (valueSet != null) {
            // Calculate for a value setL first, figure out to which index the position corresponds to.
            // This is pretty much the calculation for a linear range from 0 to valueSet.length
            int value = (int) ((position - paddingLeft - thumbSize / 2) * (valueSet.length - 1)
                    / (getWidth() - paddingLeft - paddingRight - thumbSize));
            // Limit to the bounds of the array
            if (value < 0) {
                return valueSet[0];
            } else if (value >= valueSet.length) {
                return valueSet[valueSet.length - 1];
            } else {
                return valueSet[value];
            }
        }

        // Calculate for a linear range
        int value = (int) ((position - paddingLeft - thumbSize / 2) * (maxValue - minValue)
                / (getWidth() - paddingLeft - paddingRight - thumbSize) + minValue);
        if (value < minValue) {
            return minValue;
        }
        if (value > maxValue) {
            return maxValue;
        }
        return value;
    }

    /**
     * Quick'n'Dirty hack to return the number of the digits in the maximum possible value. This is probably possible
     * with a simple calculation but I want to avoid logarithmic.
     * @return number of digits in the maximum value
     */
    private int getMaxDigits() {
        final int maxValue;
        if (valueSet != null) {
            maxValue = valueSet[valueSet.length - 1];
        } else {
            maxValue = this.value;
        }
        return maxValue < 10 ? 1
                : maxValue < 100 ? 2
                        : maxValue < 1000 ? 3
                                : maxValue < 10000 ? 4
                                        : maxValue < 100000 ? 5
                                                : maxValue < 1000000 ? 6
                                                        : maxValue < 10000000 ? 7 : maxValue < 100000000 ? 8 : 9;
    }

    /** (Pre)Calculates the size of the thumb and updates {@link #thumbSize} */
    private void calculateThumbSize() {
        final int maxValue;
        if (valueSet != null) {
            maxValue = valueSet[valueSet.length - 1];
        } else {
            maxValue = this.maxValue;
        }
        // Ok, here's another Quick'n'Dirty hack: I don't know what the widest digit is in the different locales. For
        // now I assume it's the "9", Maybe it doesn't even matter. More likely this needs to be done a lot smarter.
        // We generate a string formatted according to "thumbFormat" with a number that has as many digits as the
        // maximum value but with all digits "9"
        final String demoValue = String.format(Locale.getDefault(), thumbFormat, maxValue < 10 ? 9
                : maxValue < 100 ? 99
                        : maxValue < 1000 ? 999
                                : maxValue < 10000 ? 9999
                                        : maxValue < 100000 ? 99999
                                                : maxValue < 1000000 ? 999999
                                                        : maxValue < 10000000 ? 9999999
                                                                : maxValue < 100000000 ? 99999999 : 999999999);

        // Calculate the width of that string + padding on each side
        final float valueWidth = 1.4142136f
                * ((thumbTextPaint.measureText(demoValue, 0, demoValue.length())) + 2 * thumbPadding);

        // Calculate the height of that string + padding on top and bottom
        final float valueTotalHeight = 1.4142136f
                * ((-thumbTextPaint.ascent() + thumbTextPaint.descent()) + 2 * thumbPadding);

        thumbSize = valueWidth > valueTotalHeight ? valueWidth : valueTotalHeight;
        //      // The thumb size is the diameter of the circle that encloses the rectangle of the box
        //      thumbSize = (float) (Math.sqrt(valueWidth * valueWidth + valueTotalHeight + valueTotalHeight));
    }

    /**
     * Read out the attributes from the given attribute set and initialize whatever they represent.
     *
     * @param attributeArray typed array containing the attribute values from the XML file
     */
    private void setupAttributes(TypedArray attributeArray) {
        minValue = attributeArray.getInt(R.styleable.RangeSlider_minValue, 0);
        maxValue = attributeArray.getInt(R.styleable.RangeSlider_maxValue, 100);

        thumbPadding = attributeArray.getDimension(R.styleable.RangeSlider_thumbPadding,
                6 /* dp */ * displayDensity);

        thumbFormat = attributeArray.getString(R.styleable.RangeSlider_thumbFormat);
        if (thumbFormat == null) {
            thumbFormat = "%d";
        }

        String valueSetString = attributeArray.getString(R.styleable.RangeSlider_valueSet);
        if (valueSetString == null) {
            value = attributeArray.getInt(R.styleable.RangeSlider_android_value, minValue);
        } else {
            String[] values = valueSetString.split(",");
            valueSet = new int[values.length];
            for (int no = 0; no < values.length; no++) {
                valueSet[no] = Integer.valueOf(values[no]);
            }
            Arrays.sort(valueSet);
            value = attributeArray.getInt(R.styleable.RangeSlider_android_value, valueSet[0]);
        }

        thumbTextPaint.setTextSize(attributeArray.getDimension(R.styleable.RangeSlider_android_textSize, 12));
        thumbTextPaint.setColor(attributeArray.getColor(R.styleable.RangeSlider_android_textColor,
                ContextCompat.getColor(getContext(), android.R.color.white)));

        thumbBackgroundPaint.setStyle(Paint.Style.FILL);
        thumbBackgroundPaint.setColor(attributeArray.getColor(R.styleable.RangeSlider_android_color,
                ContextCompat.getColor(getContext(), android.R.color.black)));

        sliderPaint.setStrokeWidth(attributeArray.getDimension(R.styleable.RangeSlider_sliderThickness, 2));
        sliderPaint.setStrokeCap(Paint.Cap.ROUND);
        sliderPaint.setColor(thumbBackgroundPaint.getColor());

    }

    /**
     * Constructor that is called when this view is created from code.
     *
     * @param context context the view is running in, through which it can access the current theme, resources, etc.
     */
    public RangeSlider(Context context) {
        super(context);

        thumbPath.setFillType(Path.FillType.EVEN_ODD);

        calculateThumbSize();
    }

    /**
     * Constructor that is called when inflating a view from XML.
     * This is called when a view is being constructed from an XML file, supplying attributes that were specified in the
     * XML file. This version uses a default style of 0, so the only attribute values applied are those in the Context's
     * Theme and the given AttributeSet.
     *
     * @param context context the view is running in, through which it can access the current theme, resources, etc.
     * @param attrs   the attributes of the XML tag that is inflating the vie
     */
    public RangeSlider(Context context, AttributeSet attrs) {
        super(context, attrs);

        thumbPath.setFillType(Path.FillType.EVEN_ODD);
        displayDensity = getResources().getDisplayMetrics().density;

        TypedArray attributeArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RangeSlider, 0, 0);
        try {
            setupAttributes(attributeArray);
        } finally {
            attributeArray.recycle();
        }

        calculateThumbSize();
    }

    /**
     * Perform inflation from XML and apply a class-specific base style from a theme attribute or style resource.
     * This constructor of View allows subclasses to use their own base style when they are inflating.
     *
     * When determining the final value of a particular attribute, there are four inputs that come into play:
     * <ul>
     *   <li>Any attribute values in the given AttributeSet.</li>
     *   <li>The style resource specified in the {@code AttributeSet} (named "style").</li>
     *   <li>The default style specified by {@code defStyleAttr}.</li>
     *   <li>he default style specified by {@code defStyleRes}.</li>T
     *   <li>The base values in this theme.</li>
     * </ul>
     *
     * Each of these inputs is considered in-order, with the first listed taking precedence over the following ones.
     *
     * @param context      context the view is running in, through which it can access the current theme, resources, etc.
     * @param attrs        the attributes of the XML tag that is inflating the vie
     * @param defStyleAttr An attribute in the current theme that contains a reference to a style resource that supplies
     *                     default values for the view. Can be 0 to not look for defaults.
     * @param defStyleRes  A resource identifier of a style resource that supplies default values for the view, used
     *                     only if {@code defStyleAttr} is 0 or can not be found in the theme. Can be 0 to not look for
     *                     defaults.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public RangeSlider(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        thumbPath.setFillType(Path.FillType.EVEN_ODD);
        displayDensity = getResources().getDisplayMetrics().density;

        TypedArray attributeArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RangeSlider,
                defStyleAttr, defStyleRes);
        try {
            setupAttributes(attributeArray);
        } finally {
            attributeArray.recycle();
        }

        calculateThumbSize();
    }

    /**
     * Called whenever the size of this view has changed.
     *
     * We are merely resetting the thumb path so that it gets redone when we draw next time.
     *
     * @param newWidth  width of the view after the size changed
     * @param newHeight height of the view after the size changed
     * @param oldWidth  width of the view before the size changed
     * @param oldHeight height of the view before the size changed
     */
    @Override
    protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        thumbPath.reset();
    }

    /**
     * Allows the user to click on the thumb. Clicking shows an alert that allows the user to enter a value. The
     * entered value is adjusted to the minimum/maximum (or to be one of the value set if one is active).
     *
     * @return {@code true} to indicate that the click was handled.
     */
    @Override
    public boolean performClick() {
        super.performClick();

        final Context c = this.getContext();
        final AlertDialog.Builder alert = new AlertDialog.Builder(c);
        final EditText input = new EditText(c);
        input.setInputType(InputType.TYPE_CLASS_NUMBER);
        input.setEms(getMaxDigits());
        alert.setView(input).setTitle(getContext().getString(R.string.enter_new_value_title))
                .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        try {
                            int enteredValue = Integer.valueOf(input.getText().toString().trim());
                            int newValue;
                            if (valueSet != null) {
                                newValue = valueSet[valueSet.length - 1];
                                for (int searchValue : valueSet) {
                                    if (enteredValue <= searchValue) {
                                        newValue = searchValue;
                                        break;
                                    }
                                }
                            } else {
                                newValue = enteredValue < minValue ? minValue
                                        : enteredValue > maxValue ? maxValue : enteredValue;
                            }
                            if (newValue != enteredValue) {
                                Toast.makeText(c, c.getString(R.string.entered_value_adjusted, newValue),
                                        Toast.LENGTH_LONG).show();
                            }
                            if (newValue != value) {
                                updateValue(newValue, true /* from user */);
                            }
                        } catch (NumberFormatException e) {
                            Toast.makeText(c, R.string.invalid_value, Toast.LENGTH_SHORT).show();
                        }
                    }
                }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        dialog.cancel();
                    }
                }).show();

        return true;
    }

    /**
     * Handle touch events: drag the thumb to new values. This method also detects clicks.
     *
     * @param event the touch event
     * @return {@code true} if the event was handled; {@code false} otherwise
     */
    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
            Log.d("Wa-Tor", "Got touch event: " + event);
        }
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            touchEventStartTime = System.currentTimeMillis();
            valueBeforeDragging = value;
            activePointerId = event.getPointerId(0);
            float valuePos = positionFromValue(paddingLeft, paddingRight);
            float x = event.getX();
            if (x < valuePos - thumbSize / 2 || x > valuePos + thumbSize / 2) {
                int newValue = valueFromPosition(x, paddingLeft, paddingRight);
                if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
                    Log.d("Wa-Tor", "Starting to drag thumb OUTSIDE of thumb");
                }
                if (newValue != value) {
                    touchEventStartTime = 0L; // Not a click
                    updateValue(newValue, true /* from user */);
                }
                touchOffset = 0f;
            } else {
                touchOffset = x - valuePos;
                if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
                    Log.d("Wa-Tor", "Starting to drag thumb INSIDE of thumb; offset = " + touchOffset);
                }
            }
            isDragging = true;
            return true;
        case MotionEvent.ACTION_MOVE:
            if (activePointerId != -1) {
                final int pointerIndex = event.findPointerIndex(activePointerId);
                float currentPos = event.getX(pointerIndex) - touchOffset;
                int newValue = valueFromPosition(currentPos, paddingLeft, paddingRight);
                if (newValue != value) {
                    if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
                        Log.d("Wa-Tor", "Got new value " + newValue + " (old = " + value + ")");
                    }
                    touchEventStartTime = 0L; // Not a click
                    updateValue(newValue, true /* from user */);
                }
            }
            return true;
        case MotionEvent.ACTION_UP:
            if (touchEventStartTime > 0L && System.currentTimeMillis() - touchEventStartTime < MAX_CLICK_DURATION) {
                performClick();
            }
            isDragging = false;
            break;
        case MotionEvent.ACTION_CANCEL:
            isDragging = false;
            updateValue(valueBeforeDragging, true /* from user */);
            break;
        case MotionEvent.ACTION_POINTER_UP:
            isDragging = false;
            break;
        }

        return super.onTouchEvent(event);
    }

    /**
     * Implement custom measuring of the view.
     *
     * @param widthMeasureSpec  horizontal space requirements as imposed by the parent. The requirements are encoded with
     *                          {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent. The requirements are encoded with
     *                          {@link android.view.View.MeasureSpec}.
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        final int height;
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            int paddingTop = getPaddingTop();
            int paddingBottom = getPaddingBottom();
            final float totalHeight = thumbSize + paddingTop + paddingBottom;
            if (heightMode == MeasureSpec.UNSPECIFIED || totalHeight < heightSize) {
                height = (int) (totalHeight + 1f); // round up
            } else {
                height = heightSize;
            }
        }
        setMeasuredDimension(widthSize, height);
    }

    /**
     * Draws the view
     * @param canvas canvas to paint on
     */
    @Override
    protected void onDraw(Canvas canvas) {
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();
        int sliderY = (getHeight() - paddingTop - paddingBottom) / 2 + paddingTop;

        canvas.drawLine(paddingLeft + thumbSize / 2, sliderY, getWidth() - paddingRight - thumbSize / 2, sliderY,
                sliderPaint);

        String valueString = String.format(Locale.getDefault(), thumbFormat, value);
        final float valueWidth = (thumbTextPaint.measureText(valueString, 0, valueString.length()));

        float thumbTip = positionFromValue(paddingLeft, paddingRight);
        if (thumbPath.isEmpty()) {
            thumbPath.addCircle(thumbTip, sliderY, thumbSize / 2, Path.Direction.CW);
        } else {
            thumbPath.offset(thumbTip, 0);
        }

        canvas.drawPath(thumbPath, thumbBackgroundPaint);

        canvas.drawText(valueString, thumbTip - valueWidth / 2, sliderY + (-thumbTextPaint.ascent()) / 2,
                thumbTextPaint);

        thumbPath.offset(-thumbTip, 0);
    }

    /** @return minimum value of this slider */
    public int getMinValue() {
        return minValue;
    }

    /**
     * Changes the minimum value of this slider. Note: if the current value is smaller than the new minimum value
     * the value is adjusted accordingly.
     *
     * @param minValue new minimum value
     */
    public void setMinValue(int minValue) {
        if (minValue != this.minValue) {
            this.minValue = minValue;
            calculateThumbSize();
            if (value < minValue) {
                updateValue(minValue, false /* from user */);
            } else {
                invalidate();
            }
        }
    }

    /**
     * @return maximum value of this slider
     */
    public int getMaxValue() {
        return maxValue;
    }

    /**
     * Changes the maximum value of this slider. Note: if the current value is smaller than the new minimum value
     * the value is adjusted accordingly.
     *
     * @param maxValue new maximum value
     */
    public void setMaxValue(int maxValue) {
        if (maxValue != this.maxValue) {
            this.maxValue = maxValue;
            if (value > maxValue) {
                updateValue(maxValue, true /* from user */);
            }
            invalidate();
        }
    }

    /** @return text size used to draw the thumb value */
    public float getTextSize() {
        return thumbTextPaint.getTextSize();
    }

    /**
     * Changes the text size that is used to draw the thumb value
     *
     * @param textSize new text size for the thumb value
     */
    public void setTextSize(float textSize) {
        if (textSize != thumbTextPaint.getTextSize()) {
            thumbTextPaint.setTextSize(textSize);
            invalidate();
        }
    }

    /** @return current value of the slider */
    public int getValue() {
        return value;
    }

    /**
     * Changes the value of the slider. Note: the value is adusted to be in between the minimum and the maximum
     * (or to be one of the value set values if a set is active)
     *
     * @param value new value for the slider
     */
    public void setValue(int value) {
        if (valueSet != null) {

            // Value set

            if (value != this.value) {
                // Set the value to one of the value set
                int foundValue = valueSet[valueSet.length - 1];
                // Make sure that the final value is one in our value set
                for (int valueFromSet : valueSet) {
                    if (value <= valueFromSet) {
                        foundValue = valueFromSet;
                        break;
                    }
                }
                if (foundValue != this.value) {
                    updateValue(foundValue, false /* from user */);
                }
            }
        } else {

            // Linear range: limit to the range

            if (value < minValue) {
                value = minValue;
            } else if (value > maxValue) {
                value = maxValue;
            }
            if (value != this.value) {
                updateValue(value, false /* from user */);
            }
        }
    }

    /** @return padding of the thumb */
    public float getThumbPadding() {
        return thumbPadding / displayDensity;
    }

    /**
     * Changes the padding of the thumb
     *
     * @param thumbPadding new padding (on each side)
     */
    public void setThumbPadding(float thumbPadding) {
        this.thumbPadding = thumbPadding * displayDensity;
    }

    /**
     * Sets an {@link RangeSlider.OnValueChangeListener} that is called whenever the value of this slider changes. Calling this
     * method replaces a previously set listener.
     *
     * @param onValueChangeListener new listener
     */
    synchronized public void setOnValueChangeListener(OnValueChangeListener onValueChangeListener) {
        this.onValueChangeListener = onValueChangeListener;
    }

    /** @return currently active {@link RangeSlider.OnValueChangeListener} */
    public OnValueChangeListener getOnValueChangeListener() {
        return onValueChangeListener;
    }

}