package hearsilent.discreteslider;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import hearsilent.discreteslider.libs.MoveGestureDetector;
import hearsilent.discreteslider.libs.Utils;

public class DiscreteSlider extends View {

	public static final int HORIZONTAL = 0;
	public static final int VERTICAL = 1;

	public static final int TOP = 0;
	public static final int RIGHT = 90;
	public static final int BOTTOM = 180;
	public static final int LEFT = 270;

	public static final int MODE_NORMAL = 0;
	public static final int MODE_RANGE = 1;

	private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	private RectF mRectF = new RectF();

	private float mOffset = 0f;
	private float mMinOffset, mMaxOffset;

	private float mRadius, mTrackWidth;

	private int mTrackColor;
	private int mInactiveTrackColor;
	private int mThumbColor;
	private int mThumbPressedColor;

	private List<Object> mTickMarkPatterns;
	private int mTickMarkColor;
	private int mTickMarkInactiveColor;
	private int mTickMarkStep;

	private int mValueLabelTextColor;

	private int mCount;
	private int mProgressOffset = 0;
	private int mMinProgress = 0, mTmpMinProgress = 0, mMaxProgress = -1, mTmpMaxProgress = -1;
	private int mPendingPosition = -1, mPressedPosition = -1;

	@Mode private int mMode = MODE_NORMAL;

	private Path mInactiveTrackPath = new Path();

	private MoveGestureDetector mMoveDetector;
	private float mValueLabelTextSize;
	private ValueLabelFormatter mValueLabelFormatter;
	private Rect mBounds = new Rect();
	private Path mValueLabelPath = new Path();
	private ValueAnimator mValueLabelAnimator;
	private Matrix mValueLabelMatrix = new Matrix();
	private float mValueLabelAnimValue = 0f;
	@ValueLabelGravity private int mValueLabelGravity;
	private int mValueLabelMode = 1;
	private int mValueLabelDuration = 1500;
	private Handler mShowValueLabelHandler = new Handler();
	private boolean mValueLabelIsShowing = false;

	private boolean mSkipMove;

	private float mLength;
	@OrientationMode private int mOrientation;

	private OnValueChangedListener mListener;
	private boolean mValueChangedImmediately = false;

	@IntDef({MODE_NORMAL, MODE_RANGE}) @Retention(RetentionPolicy.SOURCE) private @interface Mode {

	}

	@IntDef({TOP, RIGHT, BOTTOM, LEFT}) @Retention(RetentionPolicy.SOURCE)
	private @interface ValueLabelGravity {

	}

	@IntDef({HORIZONTAL, VERTICAL}) @Retention(RetentionPolicy.SOURCE)
	private @interface OrientationMode {

	}

	public DiscreteSlider(Context context) {
		super(context);
		init(context, null);
	}

	public DiscreteSlider(Context context, @Nullable AttributeSet attrs) {
		super(context, attrs);
		init(context, attrs);
	}

	public DiscreteSlider(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init(context, attrs);
	}

	private void init(Context context, @Nullable AttributeSet attrs) {
		mPaint.setStyle(Paint.Style.FILL);

		if (attrs != null) {
			TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSlider);

			mTrackWidth = a.getDimension(R.styleable.DiscreteSlider_ds_trackWidth,
					Utils.convertDpToPixel(4, getContext()));
			mTrackWidth = Math.max(mTrackWidth, Float.MIN_VALUE);
			mTrackColor = a.getColor(R.styleable.DiscreteSlider_ds_trackColor, 0xff5123da);
			mInactiveTrackColor =
					a.getColor(R.styleable.DiscreteSlider_ds_inactiveTrackColor, 0x3d5123da);

			mRadius = a.getDimension(R.styleable.DiscreteSlider_ds_thumbRadius,
					Utils.convertDpToPixel(6, getContext()));
			mRadius = Math.max(mRadius, Float.MIN_VALUE);
			mThumbColor = a.getColor(R.styleable.DiscreteSlider_ds_thumbColor, 0xff5123da);
			mThumbPressedColor =
					a.getColor(R.styleable.DiscreteSlider_ds_thumbPressedColor, 0x1f5123da);

			mTickMarkColor = a.getColor(R.styleable.DiscreteSlider_ds_tickMarkColor, 0xff9972ed);
			mTickMarkInactiveColor =
					a.getColor(R.styleable.DiscreteSlider_ds_tickMarkInactiveColor, 0xff936ce2);
			mTickMarkStep = a.getInteger(R.styleable.DiscreteSlider_ds_tickMarkStep, 1);

			mValueLabelTextColor =
					a.getColor(R.styleable.DiscreteSlider_ds_valueLabelTextColor, Color.WHITE);
			mValueLabelTextSize = a.getDimension(R.styleable.DiscreteSlider_ds_valueLabelTextSize,
					Utils.convertSpToPixel(16, context));
			mValueLabelGravity = a.getInt(R.styleable.DiscreteSlider_ds_valueLabelGravity, TOP);
			mValueLabelMode = a.getInt(R.styleable.DiscreteSlider_ds_valueLabelMode, 1);
			mValueLabelDuration = a.getInt(R.styleable.DiscreteSlider_ds_valueLabelDuration, 1500);
			mValueLabelDuration = Math.max(mValueLabelDuration, 500);

			mCount = a.getInt(R.styleable.DiscreteSlider_ds_count, 11);
			mCount = Math.max(mCount, 2);
			mMode = a.getInt(R.styleable.DiscreteSlider_ds_mode, MODE_NORMAL);

			if (1 > mTickMarkStep || (mCount - 1) % mTickMarkStep != 0) {
				mTickMarkStep = 1;
			}

			mProgressOffset = a.getInt(R.styleable.DiscreteSlider_ds_progressOffset, 0);
			mTmpMinProgress = mMinProgress = a.getInt(R.styleable.DiscreteSlider_ds_progress,
					a.getInt(R.styleable.DiscreteSlider_ds_minProgress, 0));
			if (mMode == MODE_NORMAL) {
				mTmpMaxProgress = mMaxProgress = -1;
			} else {
				mTmpMaxProgress = mMaxProgress =
						a.getInt(R.styleable.DiscreteSlider_ds_maxProgress, mCount - 1);
			}

			if (a.hasValue(R.styleable.DiscreteSlider_ds_tickMarkPatterns)) {
				String patterns = a.getString(R.styleable.DiscreteSlider_ds_tickMarkPatterns);
				if (!TextUtils.isEmpty(patterns)) {
					float length = a.getDimension(R.styleable.DiscreteSlider_ds_tickMarkDashLength,
							Utils.convertDpToPixel(1, context));
					mTickMarkPatterns = new ArrayList<>();
					if (patterns.contains(",")) {
						for (String pattern : patterns.split(",")) {
							if (pattern.equalsIgnoreCase("dot")) {
								mTickMarkPatterns.add(new Dot());
							} else if (pattern.equalsIgnoreCase("dash")) {
								mTickMarkPatterns.add(new Dash(length));
							}
						}
					} else {
						if (patterns.equalsIgnoreCase("dot")) {
							mTickMarkPatterns.add(new Dot());
						} else if (patterns.equalsIgnoreCase("dash")) {
							mTickMarkPatterns.add(new Dash(length));
						}
					}
				}
				generateInactiveTrackPath();
			}

			mOrientation = a.getInt(R.styleable.DiscreteSlider_ds_orientation, HORIZONTAL);

			if (mOrientation == HORIZONTAL &&
					(mValueLabelGravity != TOP && mValueLabelGravity != BOTTOM)) {
				mValueLabelGravity = TOP;
			} else if (mOrientation == VERTICAL &&
					(mValueLabelGravity != RIGHT && mValueLabelGravity != LEFT)) {
				mValueLabelGravity = RIGHT;
			}

			setMode(mMode);

			a.recycle();
		} else {
			mTrackWidth = Utils.convertDpToPixel(4, context);
			mTrackColor = 0xff5123da;
			mInactiveTrackColor = 0x3d5123da;

			mRadius = Utils.convertDpToPixel(6, context);
			mThumbColor = 0xff5123da;
			mThumbPressedColor = 0x1f5123da;

			mTickMarkColor = 0xff9972ed;
			mTickMarkInactiveColor = 0xff936ce2;
			mTickMarkStep = 1;

			mValueLabelTextSize = Utils.convertSpToPixel(16, context);
			mValueLabelTextColor = Color.WHITE;
			mValueLabelGravity = TOP;

			mOrientation = HORIZONTAL;

			mCount = 11;
		}

		setValueLabelFormatter(new ValueLabelFormatter() {

			@Override
			public String getLabel(int input) {
				return Integer.toString(input);
			}
		});

		mMoveDetector = new MoveGestureDetector(context, new MoveListener());
	}

	public void setTrackWidth(@FloatRange(from = Float.MIN_VALUE) float trackWidth) {
		if (trackWidth <= 0) {
			throw new IllegalArgumentException("Track width must be a positive number.");
		}
		mTrackWidth = trackWidth;
		generateInactiveTrackPath();
		invalidate();
	}

	public float getTrackWidth() {
		return mTrackWidth;
	}

	public void setTrackColor(@ColorInt int trackColor) {
		mTrackColor = trackColor;
		invalidate();
	}

	@ColorInt
	public int getTrackColor() {
		return mInactiveTrackColor;
	}

	public void setInactiveTrackColor(@ColorInt int inactiveTrackColor) {
		mInactiveTrackColor = inactiveTrackColor;
		invalidate();
	}

	@ColorInt
	public int getInactiveTrackColor() {
		return mInactiveTrackColor;
	}

	public void setThumbRadius(@FloatRange(from = Float.MIN_VALUE) float radius) {
		if (radius <= 0) {
			throw new IllegalArgumentException("Thumb radius must be a positive number.");
		}
		mRadius = radius;
		generateInactiveTrackPath();
		invalidate();
	}

	public float getThumbRadius() {
		return mRadius;
	}

	public void setThumbColor(@ColorInt int thumbColor) {
		mThumbColor = thumbColor;
		invalidate();
	}

	@ColorInt
	public int getThumbColor() {
		return mThumbColor;
	}

	public void setThumbPressedColor(@ColorInt int thumbPressedColor) {
		mThumbPressedColor = thumbPressedColor;
		invalidate();
	}

	@ColorInt
	public int getThumbPressedColor() {
		return mThumbPressedColor;
	}

	public void setTickMarkColor(@ColorInt int tickMarkColor) {
		mTickMarkColor = tickMarkColor;
		invalidate();
	}

	@ColorInt
	public int getTickMarkColor() {
		return mTickMarkColor;
	}

	public void setTickMarkInactiveColor(@ColorInt int tickMarkInactiveColor) {
		mTickMarkInactiveColor = tickMarkInactiveColor;
		invalidate();
	}

	@ColorInt
	public int getTickMarkInactiveColor() {
		return mTickMarkInactiveColor;
	}

	public int getTickMarkStep() {
		return mTickMarkStep;
	}

	public void setTickMarkStep(int tickMarkStep) {
		if (1 > tickMarkStep) {
			throw new IllegalArgumentException("TickMark step must >= 1.");
		}
		if ((mCount - 1) % tickMarkStep != 0) {
			throw new IllegalArgumentException(
					"TickMark step must be a factor of " + (mCount - 1) + ".");
		}
		mTickMarkStep = tickMarkStep;
	}

	public void setValueLabelTextColor(@ColorInt int valueLabelTextColor) {
		mValueLabelTextColor = valueLabelTextColor;
		invalidate();
	}

	@ColorInt
	public int getValueLabelTextColor() {
		return mValueLabelTextColor;
	}

	public void setValueLabelTextSize(
			@FloatRange(from = Float.MIN_VALUE) float valueLabelTextSize) {
		if (valueLabelTextSize <= 0) {
			throw new IllegalArgumentException("Value label text size must be a positive number.");
		}
		mValueLabelTextSize = valueLabelTextSize;
		invalidate();
	}

	public float getValueLabelTextSize() {
		return mValueLabelTextSize;
	}

	public void setValueLabelGravity(@ValueLabelGravity int valueLabelGravity) {
		if (mOrientation == HORIZONTAL && valueLabelGravity != TOP && valueLabelGravity != BOTTOM) {
			throw new IllegalArgumentException(
					"Horizontal orientation value label gravity must be top or bottom.");
		} else if (mOrientation == VERTICAL && valueLabelGravity != RIGHT &&
				valueLabelGravity != LEFT) {
			throw new IllegalArgumentException(
					"Vertical orientation value label gravity must be right or left.");
		}
		mValueLabelGravity = valueLabelGravity;
		invalidate();
	}

	@ValueLabelGravity
	public int getValueLabelGravity() {
		return mValueLabelGravity;
	}

	public void setMode(@Mode int mode) {
		if (mode != MODE_RANGE && mode != MODE_NORMAL) {
			throw new IllegalArgumentException("Mode must be normal or range.");
		}
		mMode = mode;
		checkProgressBound();
		invalidate();
	}

	@Mode
	public int getMode() {
		return mMode;
	}

	public void setCount(@IntRange(from = 2) int count) {
		if (count < 2) {
			throw new IllegalArgumentException("Count must larger than 2.");
		}
		mCount = count;
		checkProgressBound();
		invalidate();
	}

	public int getCount() {
		return mCount;
	}

	public void setTickMarkPatterns(List<Object> patterns) {
		if (patterns == null) {
			mTickMarkPatterns = null;
		} else {
			for (Object pattern : patterns) {
				if (!(pattern instanceof Dot) && !(pattern instanceof Dash)) {
					throw new IllegalArgumentException("Pattern only accepted dot or dash.");
				}
			}
			mTickMarkPatterns = patterns;
		}
		generateInactiveTrackPath();
		invalidate();
	}

	@Nullable
	public List<Object> getTickMarkPatterns() {
		return mTickMarkPatterns;
	}

	public void setValueLabelFormatter(@NonNull ValueLabelFormatter formatter) {
		mValueLabelFormatter = formatter;
	}

	public ValueLabelFormatter getValueLabelFormatter() {
		return mValueLabelFormatter;
	}

	public void setValueLabelMode(int mode) {
		mValueLabelMode = mode;
		invalidate();
	}

	public int getValueLabelMode() {
		return mValueLabelMode;
	}

	public void setValueLabelDuration(@IntRange(from = 500) int duration) {
		mValueLabelDuration = duration;
		invalidate();
	}

	public int getValueLabelDuration() {
		return mValueLabelDuration;
	}

	public void setProgressOffset(int progressOffset) {
		mProgressOffset = progressOffset;
		invalidate();
	}

	public void setProgress(int progress) {
		setMinProgress(progress);
	}

	public void setMinProgress(int progress) {
		boolean isTouchOnMinProgress = mPendingPosition == mMinProgress;

		int _progress = mMinProgress;
		mMinProgress = progress;
		checkProgressBound();
		if (_progress != mMinProgress && mListener != null) {
			if (mMaxProgress != -1 && mMode != MODE_NORMAL) {
				mListener.onValueChanged(mMinProgress + mProgressOffset,
						mMaxProgress + mProgressOffset, false);
			} else {
				mListener.onValueChanged(mMinProgress + mProgressOffset, false);
			}
		}

		if ((mValueLabelMode >> 1 & 0x1) == 1 && (mPendingPosition == -1 || isTouchOnMinProgress)) {
			showMinValueLabel();
		} else if (mPendingPosition != -1) {
			if (isTouchOnMinProgress) {
				mPendingPosition = mMinProgress;
				if (mPressedPosition != -1) {
					mPressedPosition = mMinProgress;
				}
			} else {
				mPendingPosition = mMaxProgress;
				if (mPressedPosition != -1) {
					mPressedPosition = mMaxProgress;
				}
			}
			generateValueLabelPath();
		}
		checkOffsetBounds(mPressedPosition != -1);

		invalidate();
	}

	public int getProgress() {
		return getMinProgress();
	}

	public int getMinProgress() {
		return mMinProgress;
	}

	public void setMaxProgress(int progress) {
		if (mMode != MODE_RANGE) {
			throw new IllegalStateException("Set max progress must be range mode.");
		}

		boolean isTouchOnMinProgress = mPendingPosition == mMinProgress;

		int _progress = mMaxProgress;
		mMaxProgress = progress;
		checkProgressBound();
		if (_progress != mMaxProgress && mListener != null) {
			if (mMaxProgress != -1 && mMode != MODE_NORMAL) {
				mListener.onValueChanged(mMinProgress + mProgressOffset,
						mMaxProgress + mProgressOffset, false);
			}
		}

		if ((mValueLabelMode >> 1 & 0x1) == 1 && (mPendingPosition == -1 || !isTouchOnMinProgress)) {
			showMaxValueLabel();
		} else if (mPendingPosition != -1) {
			if (isTouchOnMinProgress) {
				mPendingPosition = mMinProgress;
				if (mPressedPosition != -1) {
					mPressedPosition = mMinProgress;
				}
			} else {
				mPendingPosition = mMaxProgress;
				if (mPressedPosition != -1) {
					mPressedPosition = mMaxProgress;
				}
			}
			generateValueLabelPath();
		}
		checkOffsetBounds(mPressedPosition != -1);

		invalidate();
	}

	public int getMaxProgress() {
		return mMaxProgress;
	}

	private void checkProgressBound() {
		if (mMode != MODE_NORMAL) {
			if (mMaxProgress == -1) {
				mMaxProgress = mCount - 1;
			} else if (mMaxProgress > mCount - 1) {
				mMaxProgress = mCount - 1;
			}
			if (mMinProgress >= mMaxProgress) {
				mMinProgress = mMaxProgress - 1;
			}
		} else {
			mMaxProgress = -1;
			if (mMinProgress > mCount - 1) {
				mMinProgress = mCount - 1;
			}
		}
		mTmpMinProgress = mMinProgress;
		mTmpMaxProgress = mMaxProgress;
	}

	public void setOnValueChangedListener(@Nullable OnValueChangedListener listener) {
		mListener = listener;
	}

	public void setValueChangedImmediately(boolean immediately) {
		mValueChangedImmediately = immediately;
	}

	private void generateInactiveTrackPath() {
		float radius = mTrackWidth / 2f;
		float left, top, right, bottom;
		mInactiveTrackPath.reset();
		if (mOrientation == HORIZONTAL) {
			mLength = getWidth() - getPaddingLeft() - getPaddingRight() - mRadius * 2 + mTrackWidth;
			left = getPaddingLeft() + mRadius - radius;
			top = ((getHeight() - getPaddingTop() - getPaddingBottom()) - mTrackWidth) / 2f +
					getPaddingTop();
			right = left + mLength;
			bottom = top + mTrackWidth;
			if (mTickMarkPatterns != null && mTickMarkPatterns.size() > 0) {
				if (mTickMarkPatterns.get(0) instanceof Dot) {
					mRectF.set(left, top, left + mTrackWidth, bottom);
					mInactiveTrackPath.arcTo(mRectF, 90, 180, true);
				} else {
					mInactiveTrackPath.moveTo(left, bottom);
					mInactiveTrackPath.lineTo(left, top);
				}
				if (mTickMarkPatterns.get((mCount - 1) % mTickMarkPatterns.size()) instanceof Dot) {
					mInactiveTrackPath.lineTo(right - radius, top);
					mRectF.set(right - mTrackWidth, top, right, bottom);
					mInactiveTrackPath.arcTo(mRectF, -90, 180, true);
				} else {
					mInactiveTrackPath.lineTo(right, top);
					mInactiveTrackPath.lineTo(right, bottom);
				}
				if (mTickMarkPatterns.get(0) instanceof Dot) {
					mInactiveTrackPath.lineTo(left + radius, bottom);
				} else {
					mInactiveTrackPath.lineTo(left, bottom);
				}
				mInactiveTrackPath.close();
			} else {
				mRectF.set(left, top, right, bottom);
				mInactiveTrackPath.addRoundRect(mRectF, radius, radius, Path.Direction.CW);
			}
		} else {
			mLength =
					getHeight() - getPaddingTop() - getPaddingBottom() - mRadius * 2 + mTrackWidth;
			left = ((getWidth() - getPaddingLeft() - getPaddingRight()) - mTrackWidth) / 2f +
					getPaddingLeft();
			top = getPaddingTop() + mRadius - radius;
			right = left + mTrackWidth;
			bottom = top + mLength;
			if (mTickMarkPatterns != null && mTickMarkPatterns.size() > 0) {
				if (mTickMarkPatterns.get(0) instanceof Dot) {
					mRectF.set(left, top, right, top + mTrackWidth);
					mInactiveTrackPath.arcTo(mRectF, 180, 180, true);
				} else {
					mInactiveTrackPath.moveTo(left, top);
					mInactiveTrackPath.lineTo(right, top);
				}
				if (mTickMarkPatterns.get((mCount - 1) % mTickMarkPatterns.size()) instanceof Dot) {
					mInactiveTrackPath.lineTo(right, bottom - radius);
					mRectF.set(left, bottom - mTrackWidth, right, bottom);
					mInactiveTrackPath.arcTo(mRectF, 0, 180, true);
				} else {
					mInactiveTrackPath.lineTo(right, bottom);
					mInactiveTrackPath.lineTo(left, bottom);
				}
				if (mTickMarkPatterns.get(0) instanceof Dot) {
					mInactiveTrackPath.lineTo(left, top + radius);
				} else {
					mInactiveTrackPath.lineTo(left, top);
				}
				mInactiveTrackPath.close();
			} else {
				mRectF.set(left, top, right, bottom);
				mInactiveTrackPath.addRoundRect(mRectF, radius, radius, Path.Direction.CW);
			}
		}
	}

	@Override
	public boolean performClick() {
		super.performClick();
		return false;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		performClick();
		return isEnabled() && handleTouchEvent(event);
	}

	private void requestDisallowInterceptTouchEvent(ViewParent parent, boolean isDragging) {
		if (parent != null) {
			parent.requestDisallowInterceptTouchEvent(isDragging);
			requestDisallowInterceptTouchEvent(parent.getParent(), isDragging);
		}
	}

	private boolean handleTouchEvent(MotionEvent event) {
		if (mCount < 2) {
			mMoveDetector.onTouchEvent(event);
			return true;
		}
		float length = mLength - mTrackWidth;
		if (event.getAction() == MotionEvent.ACTION_DOWN) {
			int pendingPosition = mPendingPosition;

			mOffset = 0;
			mPendingPosition = -1;
			mSkipMove = false;

			float p = mOrientation == HORIZONTAL ? event.getX() : event.getY();
			if (mMaxProgress == -1 && mMode == MODE_NORMAL) {
				float c = getPosition(length, mMinProgress, false);
				if (c - mRadius * 3.5 <= p && p <= c + mRadius * 3.5) {
					mPendingPosition = mMinProgress;
				}
			} else {
				float c1 = getPosition(length, mMinProgress, false);
				float c2 = getPosition(length, mMaxProgress, false);
				if (c1 - mRadius * 3.5 <= p && p <= c1 + mRadius * 3.5) {
					mPendingPosition = mMinProgress;
				} else if (c2 - mRadius * 3.5 <= p && p <= c2 + mRadius * 3.5) {
					mPendingPosition = mMaxProgress;
				}
			}
			if (mPendingPosition == -1) {
				mPendingPosition = (int) getClosestPosition(p, length)[0];
			}

			if (pendingPosition != mPendingPosition) {
				if (mValueLabelAnimator != null) {
					mValueLabelAnimator.cancel();
					mValueLabelAnimator = null;
				}
				mValueLabelAnimValue = 0;
				mValueLabelIsShowing = false;
				mShowValueLabelHandler.removeCallbacksAndMessages(null);
			}

			if (isClickable()) {
				if (mPendingPosition != mMinProgress &&
						!(mPendingPosition == mMaxProgress && mMode != MODE_NORMAL)) {
					animValueLabel();
				}
				if (mMaxProgress != -1 && mMode == MODE_RANGE) {
					if (Math.abs(mMinProgress - mPendingPosition) >
							Math.abs(mMaxProgress - mPendingPosition)) {
						mMaxProgress = mPendingPosition;
					} else {
						mMinProgress = mPendingPosition;
					}
				} else {
					mMinProgress = mPendingPosition;
				}
			}

			checkOffsetBounds(true);

			if (mPendingPosition == mMinProgress ||
					mPendingPosition == mMaxProgress && mMaxProgress != -1 && mMode == MODE_RANGE) {
				requestDisallowInterceptTouchEvent(getParent(), true);
			}
		} else if (event.getAction() == MotionEvent.ACTION_UP) {
			if (mPendingPosition == -1) {
				mOffset = 0;
				mMoveDetector.onTouchEvent(event);
				return true;
			}
			if (mPendingPosition != mMinProgress && mPendingPosition != mMaxProgress) {
				float p = mOrientation == HORIZONTAL ? event.getX() : event.getY();
				final int position = (int) getClosestPosition(p, length)[0];
				if (position == mPendingPosition && !mSkipMove) {
					if (mMaxProgress == -1 && mMode == MODE_NORMAL) {
						mPendingPosition = mMinProgress;
					} else {
						if (Math.abs(mMinProgress - position) <=
								Math.abs(mMaxProgress - position)) {
							mPendingPosition = mMinProgress;
						} else {
							mPendingPosition = mMaxProgress;
						}
					}

					if (mListener != null) {
						if (mMaxProgress != -1 && mMode != MODE_NORMAL) {
							if (mPendingPosition == mMinProgress) {
								mListener.onValueChanged(position + mProgressOffset,
										mMaxProgress + mProgressOffset, true);
							} else {
								mListener.onValueChanged(mMinProgress + mProgressOffset,
										position + mProgressOffset, true);
							}
						} else {
							mListener.onValueChanged(position + mProgressOffset, true);
						}
					}

					setEnabled(false);

					mOffset = 0;
					float dis = length / (mCount - 1) * (position - mPendingPosition);
					ValueAnimator animator = ValueAnimator.ofFloat(mOffset, mOffset + dis);
					animator.setInterpolator(new DecelerateInterpolator(2.5f));
					animator.setDuration(250);
					animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

						@Override
						public void onAnimationUpdate(ValueAnimator animation) {
							mOffset = (float) animation.getAnimatedValue();
							invalidate();
						}
					});
					animator.addListener(new AnimatorListenerAdapter() {

						@Override
						public void onAnimationEnd(Animator animation) {
							super.onAnimationEnd(animation);
							mOffset = 0;
							if (mPendingPosition == mMinProgress) {
								mMinProgress = position;
							} else if (mPendingPosition == mMaxProgress && mMode != MODE_NORMAL) {
								mMaxProgress = position;
							}
							mPendingPosition = -1;
							setEnabled(true);

							invalidate();
						}
					});
					animator.start();
				}
			} else {
				float p = getPosition(length, mPendingPosition, true);
				float[] closestPosition = getClosestPosition(p, length);
				float dis = closestPosition[1];
				int position = (int) closestPosition[0];

				if (mListener != null) {
					if (mMaxProgress != -1 && mMode != MODE_NORMAL) {
						if (mPendingPosition == mMinProgress) {
							mListener.onValueChanged(position + mProgressOffset,
									mMaxProgress + mProgressOffset, true);
						} else {
							mListener.onValueChanged(mMinProgress + mProgressOffset,
									position + mProgressOffset, true);
						}
					} else {
						mListener.onValueChanged(position + mProgressOffset, true);
					}
				}

				setEnabled(false);

				final int _position = position;
				ValueAnimator animator = ValueAnimator.ofFloat(mOffset, mOffset + dis);
				animator.setInterpolator(new DecelerateInterpolator(2.5f));
				animator.setDuration(250);
				animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

					@Override
					public void onAnimationUpdate(ValueAnimator animation) {
						mOffset = (float) animation.getAnimatedValue();
						invalidate();
					}
				});
				animator.addListener(new AnimatorListenerAdapter() {

					@Override
					public void onAnimationEnd(Animator animation) {
						super.onAnimationEnd(animation);
						mOffset = 0;
						if (mPendingPosition == mMinProgress) {
							mMinProgress = _position;
						} else if (mPendingPosition == mMaxProgress && mMode != MODE_NORMAL) {
							mMaxProgress = _position;
						}
						if (mValueLabelAnimator == null) {
							mPendingPosition = -1;
							setEnabled(true);
						} else {
							mPendingPosition = _position;
						}
						invalidate();
					}
				});
				animator.start();

				hideValueLabel();
			}
			mPressedPosition = -1;
			requestDisallowInterceptTouchEvent(getParent(), false);
		} else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
			if (mPendingPosition == mMinProgress || mPendingPosition == mMaxProgress) {
				setEnabled(false);

				ValueAnimator animator = ValueAnimator.ofFloat(mOffset, 0);
				animator.setInterpolator(new DecelerateInterpolator(2.5f));
				animator.setDuration(250);
				animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

					@Override
					public void onAnimationUpdate(ValueAnimator animation) {
						mOffset = (float) animation.getAnimatedValue();
						invalidate();
					}
				});
				animator.addListener(new AnimatorListenerAdapter() {

					@Override
					public void onAnimationEnd(Animator animation) {
						super.onAnimationEnd(animation);
						mOffset = 0;
						mPendingPosition = -1;
						setEnabled(true);
						invalidate();
					}
				});
				animator.start();
			} else {
				mOffset = 0;
			}

			mPendingPosition = -1;
			mPressedPosition = -1;
			requestDisallowInterceptTouchEvent(getParent(), false);
		}
		mMoveDetector.onTouchEvent(event);
		invalidate();
		return true;
	}

	private void checkOffsetBounds(boolean isTouching) {
		float length = mLength - mTrackWidth;
		float p = getPosition(length, mPendingPosition, false);
		if (mPendingPosition == mMinProgress) {
			mMinOffset = getPosition(length, 0, false) - p;
			if (mMaxProgress != -1 && mMode == MODE_RANGE) {
				mMaxOffset = getPosition(length, mMaxProgress - 1, false) - p;
			} else {
				mMaxOffset = getPosition(length, mCount - 1, false) - p;
			}
			if (isTouching) {
				mPressedPosition = mPendingPosition;
			}
		} else if (mPendingPosition == mMaxProgress && mMode != MODE_NORMAL) {
			mMinOffset = getPosition(length, mMinProgress + 1, false) - p;
			mMaxOffset = getPosition(length, mCount - 1, false) - p;
			if (isTouching) {
				mPressedPosition = mPendingPosition;
			}
		} else if (!isClickable()) {
			mPressedPosition = mPendingPosition = -1;
		}
	}

	private void animValueLabel() {
		mValueLabelIsShowing = true;
		mShowValueLabelHandler.removeCallbacksAndMessages(null);

		float value = mValueLabelAnimValue;
		if (mValueLabelAnimator != null) {
			value = mValueLabelAnimator.getAnimatedFraction();
			mValueLabelAnimator.cancel();
		}

		if (value == 1) {
			mValueLabelAnimator = null;
			generateValueLabelPath();
			return;
		}

		mValueLabelAnimator = ValueAnimator.ofFloat(value, 1);
		mValueLabelAnimator.setDuration(Math.round((250 * (1 - value))));
		mValueLabelAnimator.setInterpolator(new AccelerateInterpolator());
		mValueLabelAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				mValueLabelAnimValue = (float) animation.getAnimatedValue();
				generateValueLabelPath();
				invalidate();
			}
		});
		mValueLabelAnimator.start();
	}

	private void hideValueLabel() {
		mValueLabelIsShowing = false;
		mShowValueLabelHandler.removeCallbacksAndMessages(null);

		float value = mValueLabelAnimValue;
		if (mValueLabelAnimator != null) {
			value = mValueLabelAnimator.getAnimatedFraction();
			mValueLabelAnimator.cancel();
		}

		if (value > 0) {
			mValueLabelAnimator = ValueAnimator.ofFloat(value, 0);
			mValueLabelAnimator.setDuration(Math.round(250 * value));
			mValueLabelAnimator.setInterpolator(new DecelerateInterpolator());
			mValueLabelAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

				@Override
				public void onAnimationUpdate(ValueAnimator animation) {
					mValueLabelAnimValue = (float) animation.getAnimatedValue();
					generateValueLabelPath();
					invalidate();
				}
			});
			mValueLabelAnimator.addListener(new AnimatorListenerAdapter() {

				@Override
				public void onAnimationEnd(Animator animation) {
					super.onAnimationEnd(animation);
					mValueLabelAnimator = null;
					if (mOffset == 0) {
						mPendingPosition = -1;
						setEnabled(true);
					}

					invalidate();
				}
			});
			mValueLabelAnimator.start();
		} else {
			mValueLabelAnimator = null;
		}
	}

	private void showMinValueLabel() {
		mPendingPosition = mMinProgress;
		showValueLabel();
	}

	private void showMaxValueLabel() {
		mPendingPosition = mMaxProgress;
		showValueLabel();
	}

	private void showValueLabel() {
		animValueLabel();
		mShowValueLabelHandler.postDelayed(new Runnable() {

			@Override
			public void run() {
				hideValueLabel();
			}
		}, mValueLabelDuration - 250);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		if (mOrientation == HORIZONTAL) {
			setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
					getSize() + getPaddingTop() + getPaddingBottom());
		} else {
			setMeasuredDimension(getSize() + getPaddingLeft() + getPaddingRight(),
					getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
		}
	}

	public int getSize() {
		return (int) Math.max(Math.ceil(mRadius * 2 * 3), mTrackWidth);
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		generateInactiveTrackPath();
		invalidate();
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		boolean isValueLabelVisible =
				(mValueLabelMode & 0x1) == 1 || (mValueLabelMode >> 1 & 0x1) == 1;
		float length = mLength - mTrackWidth;
		mPaint.setColor(mInactiveTrackColor);
		canvas.drawPath(mInactiveTrackPath, mPaint);

		float min, max;
		mPaint.setColor(mTrackColor);
		if (mOrientation == HORIZONTAL) {
			float top = ((getHeight() - getPaddingTop() - getPaddingBottom()) - mTrackWidth) / 2f +
					getPaddingTop();
			float bottom = top + mTrackWidth;
			if (mMode != MODE_NORMAL && mMaxProgress != -1) {
				float left = min = getPosition(length, mMinProgress, true) - mTrackWidth / 2f;
				float right = max = getPosition(length, mMaxProgress, true) + mTrackWidth / 2f;
				mRectF.set(left, top, right, bottom);
				canvas.drawRoundRect(mRectF, mTrackWidth / 2f, mTrackWidth / 2f, mPaint);
			} else {
				float left = min = getPosition(length, 0, false) - mTrackWidth / 2f;
				float right = max = getPosition(length, mMinProgress, true) + mTrackWidth / 2f;
				mRectF.set(left, top, right, bottom);
				if (mTickMarkPatterns == null || mTickMarkPatterns.size() == 0 ||
						mTickMarkPatterns.get(0) instanceof Dot) {
					canvas.drawRoundRect(mRectF, mTrackWidth / 2f, mTrackWidth / 2f, mPaint);
				} else {
					canvas.drawRect(mRectF, mPaint);
				}
			}
		} else {
			float left = ((getWidth() - getPaddingLeft() - getPaddingRight()) - mTrackWidth) / 2f +
					getPaddingLeft();
			float right = left + mTrackWidth;
			if (mMode != MODE_NORMAL && mMaxProgress != -1) {
				float top = min = getPosition(length, mMinProgress, true) - mTrackWidth / 2f;
				float bottom = max = getPosition(length, mMaxProgress, true) + mTrackWidth / 2f;
				mRectF.set(left, top, right, bottom);
				canvas.drawRoundRect(mRectF, mTrackWidth / 2f, mTrackWidth / 2f, mPaint);
			} else {
				float top = min = getPosition(length, 0, false) - mTrackWidth / 2f;
				float bottom = max = getPosition(length, mMinProgress, true) + mTrackWidth / 2f;
				mRectF.set(left, top, right, bottom);
				if (mTickMarkPatterns == null || mTickMarkPatterns.size() == 0 ||
						mTickMarkPatterns.get(0) instanceof Dot) {
					canvas.drawRoundRect(mRectF, mTrackWidth / 2f, mTrackWidth / 2f, mPaint);
				} else {
					canvas.drawRect(mRectF, mPaint);
				}
			}
		}

		float cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2f + getPaddingLeft();
		float cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2f + getPaddingTop();

		if (mTickMarkPatterns != null && mTickMarkPatterns.size() > 0) {
			if (mOrientation == HORIZONTAL) {
				for (int i = 0; i < mCount; i++) {
					if (i % mTickMarkStep != 0) {
						continue;
					}

					Object pattern = mTickMarkPatterns.get(i % mTickMarkPatterns.size());
					cx = getPosition(length, i, false);

					if (min <= cx && cx <= max) {
						mPaint.setColor(mTickMarkColor);
					} else {
						mPaint.setColor(mTickMarkInactiveColor);
					}

					if (pattern instanceof Dot) {
						canvas.drawCircle(cx, cy, mTrackWidth / 2f, mPaint);
					} else {
						float dashLength = ((Dash) pattern).length;
						canvas.drawRect(cx - dashLength / 2f, cy - mTrackWidth / 2f,
								cx + dashLength / 2f, cy + mTrackWidth / 2f, mPaint);
					}
				}
			} else {
				for (int i = 0; i < mCount; i++) {
					if (i % mTickMarkStep != 0) {
						continue;
					}

					Object pattern = mTickMarkPatterns.get(i % mTickMarkPatterns.size());
					cy = getPosition(length, i, false);

					if (min <= cy && cy <= max) {
						mPaint.setColor(mTickMarkColor);
					} else {
						mPaint.setColor(mTickMarkInactiveColor);
					}

					if (pattern instanceof Dot) {
						canvas.drawCircle(cx, cy, mTrackWidth / 2f, mPaint);
					} else {
						float dashLength = ((Dash) pattern).length;
						canvas.drawRect(cx - mTrackWidth / 2f, cy - dashLength / 2f,
								cx + mTrackWidth / 2f, cy + dashLength / 2f, mPaint);
					}
				}
			}
		}

		if (mOrientation == HORIZONTAL) {
			cx = getPosition(length, mMinProgress, true);
		} else {
			cy = getPosition(length, mMinProgress, true);
		}

		float _cx = cx;
		float _cy = cy;
		float dp6 = Utils.convertDpToPixel(6, getContext());
		float ratio = mRadius / dp6;
		if (mOrientation == HORIZONTAL) {
			if (mValueLabelGravity == TOP) {
				_cy -= dp6 + Utils.convertDpToPixel(16, getContext()) + dp6 * 3;
				_cy = cy + (_cy - cy) * mValueLabelAnimValue * ratio;
			} else if (mValueLabelGravity == BOTTOM) {
				_cy += dp6 + Utils.convertDpToPixel(16, getContext()) + dp6 * 3;
				_cy = cy + (_cy - cy) * mValueLabelAnimValue * ratio;
			}
		} else {
			if (mValueLabelGravity == RIGHT) {
				_cx += dp6 + Utils.convertDpToPixel(16, getContext()) + dp6 * 3;
				_cx = cx + (_cx - cx) * mValueLabelAnimValue * ratio;
			} else if (mValueLabelGravity == LEFT) {
				_cx -= dp6 + Utils.convertDpToPixel(16, getContext()) + dp6 * 3;
				_cx = cx + (_cx - cx) * mValueLabelAnimValue * ratio;
			}
		}
		if (mPendingPosition == mMinProgress && mPendingPosition != -1 &&
				mValueLabelAnimValue > 0 && isValueLabelVisible) {
			mPaint.setColor(mThumbColor);
			canvas.drawPath(mValueLabelPath, mPaint);
			canvas.drawCircle(_cx, _cy, mRadius * 3 * mValueLabelAnimValue, mPaint);
			drawValueLabel(canvas, cx, cy, _cx, _cy, length);
		}

		onDrawThumb(canvas, cx, cy, mPressedPosition != -1 && mPressedPosition == mMinProgress);

		int progress;
		if (mOrientation == HORIZONTAL) {
			progress = (int) getClosestPosition(cx, length)[0];
		} else {
			progress = (int) getClosestPosition(cy, length)[0];
		}
		if (mTmpMinProgress != progress) {
			mTmpMinProgress = progress;
			if (isHapticFeedbackEnabled()) {
				performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
			}
			if (mListener != null && mValueChangedImmediately) {
				if (mMaxProgress != -1 && mMode != MODE_NORMAL) {
					mListener.onValueChanged(progress + mProgressOffset,
							mMaxProgress + mProgressOffset, true);
				} else {
					mListener.onValueChanged(progress + mProgressOffset, true);
				}
			}
		}

		if (mMaxProgress != -1 && mMode != MODE_NORMAL) {
			mPaint.setColor(mThumbColor);
			if (mOrientation == HORIZONTAL) {
				cx = getPosition(length, mMaxProgress, true);
				_cx = cx;
				progress = (int) getClosestPosition(cx, length)[0];
			} else {
				cy = getPosition(length, mMaxProgress, true);
				_cy = cy;
				progress = (int) getClosestPosition(cy, length)[0];
			}

			if (mPendingPosition == mMaxProgress && mValueLabelAnimValue > 0 &&
					isValueLabelVisible) {
				canvas.drawPath(mValueLabelPath, mPaint);
				canvas.drawCircle(_cx, _cy, mRadius * 3 * mValueLabelAnimValue, mPaint);
				drawValueLabel(canvas, cx, cy, _cx, _cy, length);
			}

			onDrawThumb(canvas, cx, cy, mPressedPosition != -1 && mPressedPosition == mMaxProgress);

			if (mTmpMaxProgress != progress) {
				mTmpMaxProgress = progress;
				if (isHapticFeedbackEnabled()) {
					performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
				}
				if (mListener != null && mValueChangedImmediately) {
					if (mMaxProgress != -1 && mMode != MODE_NORMAL) {
						mListener.onValueChanged(mMinProgress + mProgressOffset,
								progress + mProgressOffset, true);
					}
				}
			}

		}
	}

	public void onDrawThumb(Canvas canvas, float cx, float cy, boolean hasTouched) {
		if (hasTouched) {
			mPaint.setColor(mThumbPressedColor);
			canvas.drawCircle(cx, cy, mRadius * 3.5f, mPaint);
		}
		mPaint.setColor(mThumbColor);
		canvas.drawCircle(cx, cy, mRadius, mPaint);
	}

	private void drawValueLabel(Canvas canvas, float cx, float cy, float _cx, float _cy,
	                            float length) {
		if (mValueLabelGravity == TOP && _cy + mRadius * 3 * mValueLabelAnimValue > cy - mRadius) {
			return;
		} else if (mValueLabelGravity == BOTTOM &&
				_cy - mRadius * 3 * mValueLabelAnimValue < cy + mRadius) {
			return;
		} else if (mValueLabelGravity == RIGHT &&
				_cx - mRadius * 3 * mValueLabelAnimValue < cx + mRadius) {
			return;
		} else if (mValueLabelGravity == LEFT &&
				_cx + mRadius * 3 * mValueLabelAnimValue > cx - mRadius) {
			return;
		}

		String label;
		if (mOrientation == HORIZONTAL) {
			label = mValueLabelFormatter
					.getLabel((int) getClosestPosition(cx, length)[0] + mProgressOffset);
		} else {
			label = mValueLabelFormatter
					.getLabel((int) getClosestPosition(cy, length)[0] + mProgressOffset);
		}
		if (!TextUtils.isEmpty(label)) {
			mPaint.setTextSize(mValueLabelTextSize * mValueLabelAnimValue);
			mPaint.setColor(mValueLabelTextColor);
			mPaint.getTextBounds(label, 0, label.length(), mBounds);
			canvas.drawText(label, _cx - mBounds.width() / 2f - mBounds.left,
					_cy + mBounds.height() / 2f - mBounds.bottom, mPaint);
		}
	}

	private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener {

		@Override
		public boolean onMove(MoveGestureDetector detector) {
			PointF d = detector.getFocusDelta();
			if (mOrientation == HORIZONTAL) {
				mOffset += d.x;
			} else {
				mOffset += d.y;
			}
			if ((mPendingPosition == mMinProgress ||
					mPendingPosition == mMaxProgress && mMode != MODE_NORMAL) &&
					mPendingPosition != -1) {
				mOffset = Math.min(Math.max(mOffset, mMinOffset), mMaxOffset);
				generateValueLabelPath();
				if (Math.abs(mOffset) >= mRadius * 2 && !mValueLabelIsShowing &&
						(mValueLabelMode & 0x1) == 1) {
					animValueLabel();
				} else if ((mValueLabelMode & 0x1) == 1) {
					mShowValueLabelHandler.removeCallbacksAndMessages(null);
				}
			} else if (Math.abs(mOffset) >= mRadius * 3.5) {
				mSkipMove = true;
			}
			return true;
		}
	}

	private void generateValueLabelPath() {
		float r2 = Utils.convertDpToPixel(6, getContext()), cx2, cy2;
		float r1 = r2 * 3, cx1, cy1;
		float ratio = mRadius / r2;

		float length = mLength - mTrackWidth;
		if (mPendingPosition == mMinProgress || mMaxProgress != -1 && mMode != MODE_NORMAL) {
			if (mOrientation == HORIZONTAL) {
				cy2 = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2f + getPaddingTop();
				cx2 = getPosition(length, mPendingPosition, true);
			} else {
				cy2 = getPosition(length, mPendingPosition, true);
				cx2 = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2f + getPaddingLeft();
			}
		} else {
			mValueLabelPath.reset();
			return;
		}
		cx1 = cx2;
		cy1 = cy2;

		float dp1 = Utils.convertDpToPixel(1, getContext()), dp16 =
				Utils.convertDpToPixel(16, getContext());
		float ox1, oy1, ox2, oy2;
		if (mValueLabelGravity == TOP) {
			cy1 -= r2 + dp16 + r1;
			ox1 = -Math.max((r2 - dp1), 0);
			ox2 = -ox1;
			oy1 = oy2 = r1 + dp16 / 2;
		} else if (mValueLabelGravity == BOTTOM) {
			cy1 += r2 + dp16 + r1;
			ox1 = Math.max((r2 - dp1), 0);
			ox2 = -ox1;
			oy1 = oy2 = -r1 - dp16 / 2;
		} else if (mValueLabelGravity == RIGHT) {
			cx1 += r2 + dp16 + r1;
			ox1 = ox2 = -r1 - dp16 / 2;
			oy1 = -Math.max((r2 - dp1), 0);
			oy2 = -oy1;
		} else {
			cx1 -= r2 + dp16 + r1;
			ox1 = ox2 = r1 + dp16 / 2;
			oy1 = Math.max((r2 - dp1), 0);
			oy2 = -oy1;
		}

		if (mValueLabelGravity == TOP && cy1 + r1 >= cy2 - r2) {
			mValueLabelPath.reset();
			return;
		} else if (mValueLabelGravity == BOTTOM && cy1 - r1 <= cy2 + r2) {
			mValueLabelPath.reset();
			return;
		} else if (mValueLabelGravity == RIGHT && cx1 - r1 <= cx2 + r2) {
			mValueLabelPath.reset();
			return;
		} else if (mValueLabelGravity == LEFT && cx1 + r1 >= cx2 - r2) {
			mValueLabelPath.reset();
			return;
		}

		mValueLabelPath.reset();
		mRectF.set(cx1 - r1, cy1 - r1, cx1 + r1, cy1 + r1);
		mValueLabelPath.arcTo(mRectF, 135 + mValueLabelGravity, 270, true);
		mValueLabelPath.quadTo(cx1 + ox1, cy1 + oy1,
				cx2 + r2 * (float) Math.cos(Math.toRadians(-45 + mValueLabelGravity)),
				cy2 + r2 * (float) Math.sin(Math.toRadians(-45 + mValueLabelGravity)));
		mRectF.set(cx2 - r2, cy2 - r2, cx2 + r2, cy2 + r2);
		mValueLabelPath.arcTo(mRectF, -45 + mValueLabelGravity, 270, true);
		mValueLabelPath.quadTo(cx1 + ox2, cy1 + oy2,
				cx1 + r1 * (float) Math.cos(Math.toRadians(135 + mValueLabelGravity)),
				cy1 + r1 * (float) Math.sin(Math.toRadians(135 + mValueLabelGravity)));
		mValueLabelPath
				.moveTo(cx1 + r1 * (float) Math.cos(Math.toRadians(135 + mValueLabelGravity)),
						cy1 + r1 * (float) Math.sin(Math.toRadians(135 + mValueLabelGravity)));
		mValueLabelPath.close();

		if (mValueLabelAnimValue * ratio != 1) {
			mValueLabelPath.computeBounds(mRectF, true);
			if (mOrientation == HORIZONTAL) {
				mValueLabelMatrix
						.setScale(mValueLabelAnimValue * ratio, mValueLabelAnimValue * ratio,
								mRectF.centerX(), cy2);
			} else {
				mValueLabelMatrix
						.setScale(mValueLabelAnimValue * ratio, mValueLabelAnimValue * ratio, cx2,
								mRectF.centerY());
			}
			mValueLabelPath.transform(mValueLabelMatrix);
		}
	}

	private float[] getClosestPosition(float p, float length) {
		float dis = Float.MAX_VALUE;
		int position = -1;
		for (int i = 0; i < mCount; i++) {
			float _dis = getPosition(length, i, false) - p;
			if (Math.abs(_dis) < Math.abs(dis)) {
				dis = _dis;
				position = i;
			}
		}
		return new float[]{position, dis};
	}

	private float getPosition(float length, int progress, boolean withOffset) {
		if (mOrientation == HORIZONTAL) {
			return getPaddingLeft() + length / (mCount - 1) * progress + mRadius +
					(withOffset && mPendingPosition == progress ? mOffset : 0);
		} else {
			return getPaddingTop() + length / (mCount - 1) * progress + mRadius +
					(withOffset && mPendingPosition == progress ? mOffset : 0);
		}
	}

	public static abstract class ValueLabelFormatter {

		@Nullable
		public abstract String getLabel(int input);
	}

	public static class OnValueChangedListener {

		// Only called when mode is {@Code MODE_NORMAL}
		public void onValueChanged(int progress, boolean fromUser) {

		}

		// Only called when mode is {@Code MODE_RANGE}
		public void onValueChanged(int minProgress, int maxProgress, boolean fromUser) {

		}
	}
}