package com.rey.material.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.transition.Slide;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import com.rey.material.R;
import com.rey.material.drawable.RippleDrawable;
import com.rey.material.util.ColorUtil;
import com.rey.material.util.ThemeUtil;
import com.rey.material.util.TypefaceUtil;
import com.rey.material.util.ViewUtil;
/**
* Created by Ret on 3/18/2015.
*/
public class Slider extends View{
private RippleManager mRippleManager;
private Paint mPaint;
private RectF mDrawRect;
private RectF mTempRect;
private Path mLeftTrackPath;
private Path mRightTrackPath;
private Path mMarkPath;
private int mMinValue = 0;
private int mMaxValue = 100;
private int mStepValue = 1;
private boolean mDiscreteMode = false;
private int mPrimaryColor;
private int mSecondaryColor;
private int mTrackSize;
private Paint.Cap mTrackCap;
private int mThumbBorderSize;
private int mThumbRadius;
private int mThumbFocusRadius;
private float mThumbPosition;
private Typeface mTypeface;
private int mTextSize;
private int mTextColor;
private int mGravity = Gravity.CENTER;
private int mTravelAnimationDuration;
private int mTransformAnimationDuration;
private Interpolator mInterpolator;
private int mTouchSlop;
private PointF mMemoPoint;
private boolean mIsDragging;
private float mThumbCurrentRadius;
private float mThumbFillPercent;
private int mTextHeight;
private int mMemoValue;
private String mValueText;
private ThumbRadiusAnimator mThumbRadiusAnimator;
private ThumbStrokeAnimator mThumbStrokeAnimator;
private ThumbMoveAnimator mThumbMoveAnimator;
private boolean mIsRtl = false;
/**
* Interface definition for a callback to be invoked when thumb's position changed.
*/
public interface OnPositionChangeListener{
/**
* Called when thumb's position changed.
*
* @param view The view fire this event.
* @param fromUser Indicate the change is from user touch event or not.
* @param oldPos The old position of thumb.
* @param newPos The new position of thumb.
* @param oldValue The old value.
* @param newValue The new value.
*/
public void onPositionChanged(Slider view, boolean fromUser, float oldPos, float newPos, int oldValue, int newValue);
}
private OnPositionChangeListener mOnPositionChangeListener;
public Slider(Context context) {
super(context);
init(context, null, 0, 0);
}
public Slider(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, 0);
}
public Slider(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}
public Slider(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDrawRect = new RectF();
mTempRect = new RectF();
mLeftTrackPath = new Path();
mRightTrackPath = new Path();
mThumbRadiusAnimator = new ThumbRadiusAnimator();
mThumbStrokeAnimator = new ThumbStrokeAnimator();
mThumbMoveAnimator = new ThumbMoveAnimator();
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMemoPoint = new PointF();
applyStyle(context, attrs, defStyleAttr, defStyleRes);
}
public void applyStyle(int resId){
applyStyle(getContext(), null, 0, resId);
}
private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider, defStyleAttr, defStyleRes);
mDiscreteMode = a.getBoolean(R.styleable.Slider_sl_discreteMode, mDiscreteMode);
mPrimaryColor = a.getColor(R.styleable.Slider_sl_primaryColor, ThemeUtil.colorControlActivated(context, 0xFF000000));
mSecondaryColor = a.getColor(R.styleable.Slider_sl_secondaryColor, ThemeUtil.colorControlNormal(context, 0xFF000000));
mTrackSize = a.getDimensionPixelSize(R.styleable.Slider_sl_trackSize, ThemeUtil.dpToPx(context, 2));
int cap = a.getInteger(R.styleable.Slider_sl_trackCap, 0);
if(cap == 0)
mTrackCap = Paint.Cap.BUTT;
else if(cap == 1)
mTrackCap = Paint.Cap.ROUND;
else
mTrackCap = Paint.Cap.SQUARE;
mThumbBorderSize = a.getDimensionPixelSize(R.styleable.Slider_sl_thumbBorderSize, ThemeUtil.dpToPx(context, 2));
mThumbRadius = a.getDimensionPixelSize(R.styleable.Slider_sl_thumbRadius, ThemeUtil.dpToPx(context, 10));
mThumbFocusRadius = a.getDimensionPixelSize(R.styleable.Slider_sl_thumbFocusRadius, ThemeUtil.dpToPx(context, 14));
mTravelAnimationDuration = a.getInteger(R.styleable.Slider_sl_travelAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime));
mTransformAnimationDuration = a.getInteger(R.styleable.Slider_sl_travelAnimDuration, context.getResources().getInteger(android.R.integer.config_shortAnimTime));
int resId = a.getResourceId(R.styleable.Slider_sl_interpolator, 0);
mInterpolator = resId != 0 ? AnimationUtils.loadInterpolator(context, resId) : new DecelerateInterpolator();
mGravity = a.getInt(R.styleable.Slider_android_gravity, Gravity.CENTER_VERTICAL);
mMinValue = a.getInteger(R.styleable.Slider_sl_minValue, mMinValue);
mMaxValue = a.getInteger(R.styleable.Slider_sl_maxValue, mMaxValue);
mStepValue = a.getInteger(R.styleable.Slider_sl_stepValue, mStepValue);
setValue(a.getInteger(R.styleable.Slider_sl_value, getValue()), false);
String familyName = a.getString(R.styleable.Slider_sl_fontFamily);
int style = a.getInteger(R.styleable.Slider_sl_textStyle, Typeface.NORMAL);
mTypeface = TypefaceUtil.load(context, familyName, style);
mTextColor = a.getColor(R.styleable.Slider_sl_textColor, 0xFFFFFFFF);
mTextSize = a.getDimensionPixelSize(R.styleable.Slider_sl_textSize, context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_small_material));
setEnabled(a.getBoolean(R.styleable.Slider_android_enabled, true));
a.recycle();
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTypeface(mTypeface);
measureText();
}
private void measureText(){
Rect temp = new Rect();
String text = String.valueOf(mMaxValue);
mPaint.setTextSize(mTextSize);
float width = mPaint.measureText(text);
float maxWidth = (float)(mThumbRadius * Math.sqrt(2) * 2 - ThemeUtil.dpToPx(getContext(), 8));
if(width > maxWidth){
float textSize = mTextSize * maxWidth / width;
mPaint.setTextSize(textSize);
}
mPaint.getTextBounds(text, 0, text.length(), temp);
mTextHeight = temp.height();
}
private String getValueText(){
int value = getValue();
if(mValueText == null || mMemoValue != value){
mMemoValue = value;
mValueText = String.valueOf(mMemoValue);
}
return mValueText;
}
/**
* @return The minimum selectable value.
*/
public int getMinValue(){
return mMinValue;
}
/**
* @return The maximum selectable value.
*/
public int getMaxValue(){
return mMaxValue;
}
/**
* @return The step value.
*/
public int getStepValue(){
return mStepValue;
}
/**
* Set the randge of selectable value.
* @param min The minimum selectable value.
* @param max The maximum selectable value.
* @param animation Indicate that should show animation when thumb's current position changed.
*/
public void setValueRange(int min, int max, boolean animation){
if(max < min || (min == mMinValue && max == mMaxValue))
return;
float oldValue = getExactValue();
float oldPosition = getPosition();
mMinValue = min;
mMaxValue = max;
setValue(oldValue, animation);
if(mOnPositionChangeListener != null && oldPosition == getPosition() && oldValue != getExactValue())
mOnPositionChangeListener.onPositionChanged(this, false, oldPosition, oldPosition, Math.round(oldValue), getValue());
}
/**
* @return The selected value.
*/
public int getValue(){
return Math.round(getExactValue());
}
/**
* @return The exact selected value.
*/
public float getExactValue(){
return (mMaxValue - mMinValue) * getPosition() + mMinValue;
}
/**
* @return The current position of thumb in [0..1] range.
*/
public float getPosition(){
return mThumbMoveAnimator.isRunning() ? mThumbMoveAnimator.getPosition() : mThumbPosition;
}
/**
* Set current position of thumb.
* @param pos The position in [0..1] range.
* @param animation Indicate that should show animation when change thumb's position.
*/
public void setPosition(float pos, boolean animation){
setPosition(pos, animation, animation, false);
}
private void setPosition(float pos, boolean moveAnimation, boolean transformAnimation, boolean fromUser){
boolean change = getPosition() != pos;
int oldValue = getValue();
float oldPos = getPosition();
if(!moveAnimation || !mThumbMoveAnimator.startAnimation(pos)){
mThumbPosition = pos;
if(transformAnimation) {
if(!mIsDragging)
mThumbRadiusAnimator.startAnimation(mThumbRadius);
mThumbStrokeAnimator.startAnimation(pos == 0 ? 0 : 1);
}
else{
mThumbCurrentRadius = mThumbRadius;
mThumbFillPercent = mThumbPosition == 0 ? 0 : 1;
invalidate();
}
}
int newValue = getValue();
float newPos = getPosition();
if(change && mOnPositionChangeListener != null)
mOnPositionChangeListener.onPositionChanged(this, fromUser, oldPos, newPos, oldValue, newValue);
}
/**
* Set the selected value of this Slider.
* @param value The selected value.
* @param animation Indicate that should show animation when change thumb's position.
*/
public void setValue(float value, boolean animation){
value = Math.min(mMaxValue, Math.max(value, mMinValue));
setPosition((value - mMinValue) / (mMaxValue - mMinValue), animation);
}
/**
* Set a listener will be called when thumb's position changed.
* @param listener The {@link Slider.OnPositionChangeListener} will be called.
*/
public void setOnPositionChangeListener(OnPositionChangeListener listener){
mOnPositionChangeListener = listener;
}
@Override
public void setBackgroundDrawable(Drawable drawable) {
Drawable background = getBackground();
if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
((RippleDrawable) background).setBackgroundDrawable(drawable);
else
super.setBackgroundDrawable(drawable);
}
protected RippleManager getRippleManager(){
if(mRippleManager == null){
synchronized (RippleManager.class){
if(mRippleManager == null)
mRippleManager = new RippleManager();
}
}
return mRippleManager;
}
@Override
public void setOnClickListener(OnClickListener l) {
RippleManager rippleManager = getRippleManager();
if (l == rippleManager)
super.setOnClickListener(l);
else {
rippleManager.setOnClickListener(l);
setOnClickListener(rippleManager);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.UNSPECIFIED:
widthSize = getSuggestedMinimumWidth();
break;
case MeasureSpec.AT_MOST:
widthSize = Math.min(widthSize, getSuggestedMinimumWidth());
break;
}
switch (heightMode) {
case MeasureSpec.UNSPECIFIED:
heightSize = getSuggestedMinimumHeight();
break;
case MeasureSpec.AT_MOST:
heightSize = Math.min(heightSize, getSuggestedMinimumHeight());
break;
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
public int getSuggestedMinimumWidth() {
return (mDiscreteMode ? (int)(mThumbRadius * Math.sqrt(2)) : mThumbFocusRadius) * 4 + getPaddingLeft() + getPaddingRight();
}
@Override
public int getSuggestedMinimumHeight() {
return (mDiscreteMode ? (int)(mThumbRadius * (4 + Math.sqrt(2))) : mThumbFocusRadius * 2) + getPaddingTop() + getPaddingBottom();
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
if(mIsRtl != rtl) {
mIsRtl = rtl;
invalidate();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mDrawRect.left = getPaddingLeft() + mThumbRadius;
mDrawRect.right = w - getPaddingRight() - mThumbRadius;
int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
if(mDiscreteMode){
int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2)));
int height = mThumbRadius * 2;
switch (align) {
case Gravity.TOP:
mDrawRect.top = Math.max(getPaddingTop(), fullHeight - height);
mDrawRect.bottom = mDrawRect.top + height;
break;
case Gravity.BOTTOM:
mDrawRect.bottom = h - getPaddingBottom();
mDrawRect.top = mDrawRect.bottom - height;
break;
default:
mDrawRect.top = Math.max((h - height) / 2f, fullHeight - height);
mDrawRect.bottom = mDrawRect.top + height;
break;
}
}
else{
int height = mThumbFocusRadius * 2;
switch (align) {
case Gravity.TOP:
mDrawRect.top = getPaddingTop();
mDrawRect.bottom = mDrawRect.top + height;
break;
case Gravity.BOTTOM:
mDrawRect.bottom = h - getPaddingBottom();
mDrawRect.top = mDrawRect.bottom - height;
break;
default:
mDrawRect.top = (h - height) / 2f;
mDrawRect.bottom = mDrawRect.top + height;
break;
}
}
}
private boolean isThumbHit(float x, float y, float radius){
float cx = mDrawRect.width() * mThumbPosition + mDrawRect.left;
float cy = mDrawRect.centerY();
return x >= cx - radius && x <= cx + radius && y >= cy - radius && y < cy + radius;
}
private double distance(float x1, float y1, float x2, float y2){
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
private float correctPosition(float position){
if(!mDiscreteMode)
return position;
int totalOffset = mMaxValue - mMinValue;
int valueOffset = Math.round(totalOffset * position);
int stepOffset = valueOffset / mStepValue;
int lowerValue = stepOffset * mStepValue;
int higherValue = Math.min(totalOffset, (stepOffset + 1) * mStepValue);
if(valueOffset - lowerValue < higherValue - valueOffset)
position = lowerValue / (float)totalOffset;
else
position = higherValue / (float)totalOffset;
return position;
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
super.onTouchEvent(event);
getRippleManager().onTouchEvent(event);
if(!isEnabled())
return false;
float x = event.getX();
float y = event.getY();
if(mIsRtl)
x = 2 * mDrawRect.centerX() - x;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mIsDragging = isThumbHit(x, y, mThumbRadius) && !mThumbMoveAnimator.isRunning();
mMemoPoint.set(x, y);
if(mIsDragging)
mThumbRadiusAnimator.startAnimation(mDiscreteMode ? 0 : mThumbFocusRadius);
break;
case MotionEvent.ACTION_MOVE:
if(mIsDragging) {
if(mDiscreteMode) {
float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
setPosition(position, true, true, true);
}
else{
float offset = (x - mMemoPoint.x) / mDrawRect.width();
float position = Math.min(1f, Math.max(0f, mThumbPosition + offset));
setPosition(position, false, true, true);
mMemoPoint.x = x;
invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
if(mIsDragging) {
mIsDragging = false;
setPosition(getPosition(), true, true, true);
}
else if(distance(mMemoPoint.x, mMemoPoint.y, x, y) <= mTouchSlop){
float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
setPosition(position, true, true, true);
}
break;
case MotionEvent.ACTION_CANCEL:
if(mIsDragging) {
mIsDragging = false;
setPosition(getPosition(), true, true, true);
}
break;
}
return true;
}
private void getTrackPath(float x, float y, float radius){
float halfStroke = mTrackSize / 2f;
mLeftTrackPath.reset();
mRightTrackPath.reset();
if(radius - 1f < halfStroke){
if(mTrackCap != Paint.Cap.ROUND){
if(x > mDrawRect.left){
mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke);
mLeftTrackPath.lineTo(x, y - halfStroke);
mLeftTrackPath.lineTo(x, y + halfStroke);
mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke);
mLeftTrackPath.close();
}
if(x < mDrawRect.right){
mRightTrackPath.moveTo(mDrawRect.right, y + halfStroke);
mRightTrackPath.lineTo(x, y + halfStroke);
mRightTrackPath.lineTo(x, y - halfStroke);
mRightTrackPath.lineTo(mDrawRect.right, y - halfStroke);
mRightTrackPath.close();
}
}
else{
if(x > mDrawRect.left){
mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke);
mLeftTrackPath.arcTo(mTempRect, 90, 180);
mLeftTrackPath.lineTo(x, y - halfStroke);
mLeftTrackPath.lineTo(x, y + halfStroke);
mLeftTrackPath.close();
}
if(x < mDrawRect.right){
mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke);
mRightTrackPath.arcTo(mTempRect, 270, 180);
mRightTrackPath.lineTo(x, y + halfStroke);
mRightTrackPath.lineTo(x, y - halfStroke);
mRightTrackPath.close();
}
}
}
else{
if(mTrackCap != Paint.Cap.ROUND){
mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);
if(x - radius > mDrawRect.left){
mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke);
mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke);
mLeftTrackPath.close();
}
if(x + radius < mDrawRect.right){
mRightTrackPath.moveTo(mDrawRect.right, y - halfStroke);
mRightTrackPath.arcTo(mTempRect, -angle, angle * 2);
mRightTrackPath.lineTo(mDrawRect.right, y + halfStroke);
mRightTrackPath.close();
}
}
else{
float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);
if(x - radius > mDrawRect.left){
float angle2 = (float)(Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180);
mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke);
mLeftTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2);
mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
mLeftTrackPath.close();
}
if(x + radius < mDrawRect.right){
float angle2 = (float)Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke));
mRightTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke));
angle2 = (float)(angle2 / Math.PI * 180);
mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke);
mRightTrackPath.arcTo(mTempRect, angle2, -angle2 * 2);
mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
mRightTrackPath.arcTo(mTempRect, -angle, angle * 2);
mRightTrackPath.close();
}
}
}
}
private Path getMarkPath(Path path, float cx, float cy, float radius, float factor){
if(path == null)
path = new Path();
else
path.reset();
float x1 = cx - radius;
float y1 = cy;
float x2 = cx + radius;
float y2 = cy;
float x3 = cx;
float y3 = cy + radius;
float nCx = cx;
float nCy = cy - radius * factor;
// calculate first arc
float angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
float nRadius = (float)distance(nCx, nCy, x1, y1);
mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
path.moveTo(x1, y1);
path.arcTo(mTempRect, 180 - angle, 180 + angle * 2);
if(factor > 0.9f)
path.lineTo(x3, y3);
else{
// find center point for second arc
float x4 = (x2 + x3) / 2;
float y4 = (y2 + y3) / 2;
double d1 = distance(x2, y2, x4, y4);
double d2 = d1 / Math.tan(Math.PI * (1f - factor) / 4);
nCx = (float)(x4 - Math.cos(Math.PI / 4) * d2);
nCy = (float)(y4 - Math.sin(Math.PI / 4) * d2);
// calculate second arc
angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
float angle2 = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
nRadius = (float)distance(nCx, nCy, x2, y2);
mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
path.arcTo(mTempRect, angle, angle2 - angle);
// calculate third arc
nCx = cx * 2 - nCx;
angle = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
angle2 = (float)(Math.atan2(y1 - nCy, x1 - nCx) * 180 / Math.PI);
mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
path.arcTo(mTempRect, angle + (float)Math.PI / 4, angle2 - angle);
}
path.close();
return path;
}
@Override
public void draw(@NonNull Canvas canvas) {
super.draw(canvas);
float x = mDrawRect.width() * mThumbPosition + mDrawRect.left;
if(mIsRtl)
x = 2 * mDrawRect.centerX() - x;
float y = mDrawRect.centerY();
int filledPrimaryColor = ColorUtil.getMiddleColor(mSecondaryColor, isEnabled() ? mPrimaryColor : mSecondaryColor, mThumbFillPercent);
getTrackPath(x, y, mThumbCurrentRadius);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mIsRtl ? filledPrimaryColor : mSecondaryColor);
canvas.drawPath(mRightTrackPath, mPaint);
mPaint.setColor(mIsRtl ? mSecondaryColor : filledPrimaryColor);
canvas.drawPath(mLeftTrackPath, mPaint);
mPaint.setColor(filledPrimaryColor);
if(mDiscreteMode){
float factor = 1f - mThumbCurrentRadius / mThumbRadius;
if(factor > 0){
mMarkPath = getMarkPath(mMarkPath, x, y, mThumbRadius, factor);
mPaint.setStyle(Paint.Style.FILL);
int saveCount = canvas.save();
canvas.translate(0, -mThumbRadius * 2 * factor);
canvas.drawPath(mMarkPath, mPaint);
mPaint.setColor(ColorUtil.getColor(mTextColor, factor));
canvas.drawText(getValueText(), x, y + mTextHeight / 2f - mThumbRadius * factor, mPaint);
canvas.restoreToCount(saveCount);
}
float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
if(radius > 0) {
mPaint.setColor(filledPrimaryColor);
canvas.drawCircle(x, y, radius, mPaint);
}
}
else{
float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
if(mThumbFillPercent == 1)
mPaint.setStyle(Paint.Style.FILL);
else{
float strokeWidth = (radius - mThumbBorderSize) * mThumbFillPercent + mThumbBorderSize;
radius = radius - strokeWidth / 2f;
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(strokeWidth);
}
canvas.drawCircle(x, y, radius, mPaint);
}
}
class ThumbRadiusAnimator implements Runnable{
boolean mRunning = false;
long mStartTime;
float mStartRadius;
int mRadius;
public void resetAnimation(){
mStartTime = SystemClock.uptimeMillis();
mStartRadius = mThumbCurrentRadius;
}
public boolean startAnimation(int radius) {
if(mThumbCurrentRadius == radius)
return false;
mRadius = radius;
if(getHandler() != null){
resetAnimation();
mRunning = true;
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
invalidate();
return true;
}
else {
mThumbCurrentRadius = mRadius;
invalidate();
return false;
}
}
public void stopAnimation() {
mRunning = false;
mThumbCurrentRadius = mRadius;
if(getHandler() != null)
getHandler().removeCallbacks(this);
invalidate();
}
@Override
public void run() {
long curTime = SystemClock.uptimeMillis();
float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration);
float value = mInterpolator.getInterpolation(progress);
mThumbCurrentRadius = (mRadius - mStartRadius) * value + mStartRadius;
if(progress == 1f)
stopAnimation();
if(mRunning) {
if(getHandler() != null)
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
else
stopAnimation();
}
invalidate();
}
}
class ThumbStrokeAnimator implements Runnable{
boolean mRunning = false;
long mStartTime;
float mStartFillPercent;
int mFillPercent;
public void resetAnimation(){
mStartTime = SystemClock.uptimeMillis();
mStartFillPercent = mThumbFillPercent;
}
public boolean startAnimation(int fillPercent) {
if(mThumbFillPercent == fillPercent)
return false;
mFillPercent = fillPercent;
if(getHandler() != null){
resetAnimation();
mRunning = true;
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
invalidate();
return true;
}
else {
mThumbFillPercent = mFillPercent;
invalidate();
return false;
}
}
public void stopAnimation() {
mRunning = false;
mThumbFillPercent = mFillPercent;
if(getHandler() != null)
getHandler().removeCallbacks(this);
invalidate();
}
@Override
public void run() {
long curTime = SystemClock.uptimeMillis();
float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration);
float value = mInterpolator.getInterpolation(progress);
mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent;
if(progress == 1f)
stopAnimation();
if(mRunning) {
if(getHandler() != null)
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
else
stopAnimation();
}
invalidate();
}
}
class ThumbMoveAnimator implements Runnable{
boolean mRunning = false;
long mStartTime;
float mStartFillPercent;
float mStartRadius;
float mStartPosition;
float mPosition;
float mFillPercent;
int mDuration;
public boolean isRunning(){
return mRunning;
}
public float getPosition(){
return mPosition;
}
public void resetAnimation(){
mStartTime = SystemClock.uptimeMillis();
mStartPosition = mThumbPosition;
mStartFillPercent = mThumbFillPercent;
mStartRadius = mThumbCurrentRadius;
mFillPercent = mPosition == 0 ? 0 : 1;
mDuration = mDiscreteMode && !mIsDragging ? mTransformAnimationDuration * 2 + mTravelAnimationDuration : mTravelAnimationDuration;
}
public boolean startAnimation(float position) {
if(mThumbPosition == position)
return false;
mPosition = position;
if(getHandler() != null){
resetAnimation();
mRunning = true;
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
invalidate();
return true;
}
else {
mThumbPosition = position;
invalidate();
return false;
}
}
public void stopAnimation() {
mRunning = false;
mThumbCurrentRadius = mDiscreteMode && mIsDragging ? 0 : mThumbRadius;
mThumbFillPercent = mFillPercent;
mThumbPosition = mPosition;
if(getHandler() != null)
getHandler().removeCallbacks(this);
invalidate();
}
@Override
public void run() {
long curTime = SystemClock.uptimeMillis();
float progress = Math.min(1f, (float)(curTime - mStartTime) / mDuration);
float value = mInterpolator.getInterpolation(progress);
if(mDiscreteMode){
if(mIsDragging) {
mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent;
}
else{
float p1 = (float)mTravelAnimationDuration / mDuration;
float p2 = (float)(mTravelAnimationDuration + mTransformAnimationDuration)/ mDuration;
if(progress < p1) {
value = mInterpolator.getInterpolation(progress / p1);
mThumbCurrentRadius = mStartRadius * (1f - value);
mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent;
}
else if(progress > p2){
mThumbCurrentRadius = mThumbRadius * (progress - p2) / (1 - p2);
}
}
}
else{
mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent;
if(progress < 0.2)
mThumbCurrentRadius = Math.max(mThumbRadius + mThumbBorderSize * progress * 5, mThumbCurrentRadius);
else if(progress >= 0.8)
mThumbCurrentRadius = mThumbRadius + mThumbBorderSize * (5f - progress * 5);
}
if(progress == 1f)
stopAnimation();
if(mRunning) {
if(getHandler() != null)
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
else
stopAnimation();
}
invalidate();
}
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.position = getPosition();
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setPosition(ss.position, false);
requestLayout();
}
static class SavedState extends BaseSavedState {
float position;
/**
* Constructor called from {@link Slider#onSaveInstanceState()}
*/
SavedState(Parcelable superState) {
super(superState);
}
/**
* Constructor called from {@link #CREATOR}
*/
private SavedState(Parcel in) {
super(in);
position = in.readFloat();
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeFloat(position);
}
@Override
public String toString() {
return "Slider.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " pos=" + position + "}";
}
public static final Creator<SavedState> CREATOR
= new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}