package carbon.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.animation.DecelerateInterpolator;
import carbon.Carbon;
import carbon.R;
import carbon.animation.AnimatedColorStateList;
import carbon.animation.AnimatedView;
import carbon.animation.StateAnimator;
import carbon.drawable.DefaultPrimaryColorStateList;
import carbon.drawable.ripple.RippleDrawable;
import carbon.drawable.ripple.RippleView;
import carbon.internal.MathUtils;
import carbon.internal.SeekBarPopup;
// TODO: make common carbon.widget.View class
public class SeekBar extends View implements RippleView, StateAnimatorView, AnimatedView, TintedView, VisibleView {
private static float THUMB_RADIUS, THUMB_RADIUS_DRAGGED, STROKE_WIDTH;
float value = 0.5f;
float min = 0, max = 1, step = 1;
float thumbRadius;
int tickStep = 1;
boolean tick = true;
int tickColor = 0;
boolean showLabel;
String labelFormat;
SeekBarPopup popup;
OnValueChangedListener onValueChangedListener;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private int colorControl;
private Style style;
DecelerateInterpolator interpolator = new DecelerateInterpolator();
private ValueAnimator radiusAnimator, valueAnimator;
public enum Style {
Continuous, Discrete
}
public interface OnValueChangedListener {
void onValueChanged(SeekBar seekBar, float value);
}
public SeekBar(Context context) {
super(context);
initSeekBar(null, android.R.attr.seekBarStyle);
}
public SeekBar(Context context, AttributeSet attrs) {
super(Carbon.getThemedContext(context, attrs, R.styleable.SeekBar, android.R.attr.seekBarStyle, R.styleable.SeekBar_carbon_theme), attrs);
initSeekBar(attrs, android.R.attr.seekBarStyle);
}
public SeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(Carbon.getThemedContext(context, attrs, R.styleable.SeekBar, defStyleAttr, R.styleable.SeekBar_carbon_theme), attrs, defStyleAttr);
initSeekBar(attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(Carbon.getThemedContext(context, attrs, R.styleable.SeekBar, defStyleAttr, R.styleable.SeekBar_carbon_theme), attrs, defStyleAttr, defStyleRes);
initSeekBar(attrs, defStyleAttr);
}
private static int[] rippleIds = new int[]{
R.styleable.SeekBar_carbon_rippleColor,
R.styleable.SeekBar_carbon_rippleStyle,
R.styleable.SeekBar_carbon_rippleHotspot,
R.styleable.SeekBar_carbon_rippleRadius
};
private static int[] animationIds = new int[]{
R.styleable.SeekBar_carbon_inAnimation,
R.styleable.SeekBar_carbon_outAnimation
};
private static int[] tintIds = new int[]{
R.styleable.SeekBar_carbon_tint,
R.styleable.SeekBar_carbon_tintMode,
R.styleable.SeekBar_carbon_backgroundTint,
R.styleable.SeekBar_carbon_backgroundTintMode,
R.styleable.SeekBar_carbon_animateColorChanges
};
private void initSeekBar(AttributeSet attrs, int defStyleAttr) {
if (isInEditMode())
return;
colorControl = Carbon.getThemeColor(getContext(), R.attr.colorControlNormal);
thumbRadius = THUMB_RADIUS = Carbon.getDip(getContext()) * 8;
THUMB_RADIUS_DRAGGED = Carbon.getDip(getContext()) * 10;
STROKE_WIDTH = Carbon.getDip(getContext()) * 2;
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SeekBar, defStyleAttr, R.style.carbon_SeekBar);
setStyle(Style.values()[a.getInt(R.styleable.SeekBar_carbon_barStyle, 0)]);
setMin(a.getFloat(R.styleable.SeekBar_carbon_min, 0));
setMax(a.getFloat(R.styleable.SeekBar_carbon_max, 0));
setStepSize(a.getFloat(R.styleable.SeekBar_carbon_stepSize, 0));
setValue(a.getFloat(R.styleable.SeekBar_carbon_value, 0));
setTick(a.getBoolean(R.styleable.SeekBar_carbon_tick, true));
setTickStep(a.getInt(R.styleable.SeekBar_carbon_tickStep, 1));
setTickColor(a.getColor(R.styleable.SeekBar_carbon_tickColor, 0));
setShowLabel(a.getBoolean(R.styleable.SeekBar_carbon_showLabel, false));
setLabelFormat(a.getString(R.styleable.SeekBar_carbon_labelFormat));
Carbon.initAnimations(this, a, animationIds);
Carbon.initTint(this, a, tintIds);
Carbon.initRippleDrawable(this, a, rippleIds);
a.recycle();
setFocusableInTouchMode(false); // TODO: from theme
}
@Override
protected int getSuggestedMinimumWidth() {
return Math.max((int) Math.ceil(THUMB_RADIUS_DRAGGED * 2), super.getSuggestedMinimumWidth());
}
@Override
protected int getSuggestedMinimumHeight() {
return Math.max((int) Math.ceil(THUMB_RADIUS_DRAGGED * 2), super.getSuggestedMinimumHeight());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredWidth = getSuggestedMinimumWidth();
int desiredHeight = getSuggestedMinimumHeight();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
//Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!changed)
return;
if (getWidth() == 0 || getHeight() == 0)
return;
if (rippleDrawable != null)
rippleDrawable.setBounds(0, 0, getWidth(), getHeight());
}
@Override
public void draw(@NonNull Canvas canvas) {
super.draw(canvas);
float v = (value - min) / (max - min);
int thumbX = (int) (v * (getWidth() - getPaddingLeft() - getPaddingRight()) + getPaddingLeft());
int thumbY = getHeight() / 2;
paint.setStrokeWidth(STROKE_WIDTH);
if (!isInEditMode())
paint.setColor(tint.getColorForState(getDrawableState(), tint.getDefaultColor()));
if (getPaddingLeft() < thumbX - thumbRadius)
canvas.drawLine(getPaddingLeft(), thumbY, thumbX - thumbRadius, thumbY, paint);
paint.setColor(colorControl);
if (thumbX + thumbRadius < getWidth() - getPaddingLeft())
canvas.drawLine(thumbX + thumbRadius, thumbY, getWidth() - getPaddingLeft(), thumbY, paint);
if (style == Style.Discrete && tick) {
paint.setColor(tickColor);
float range = (max - min) / step;
for (int i = 0; i < range; i += tickStep)
canvas.drawCircle(i / range * (getWidth() - getPaddingLeft() - getPaddingRight()) + getPaddingLeft(), getHeight() / 2, STROKE_WIDTH / 2, paint);
canvas.drawCircle(getWidth() - getPaddingRight(), getHeight() / 2, STROKE_WIDTH / 2, paint);
}
if (!isInEditMode())
paint.setColor(tint.getColorForState(getDrawableState(), tint.getDefaultColor()));
canvas.drawCircle(thumbX, thumbY, thumbRadius, paint);
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Over)
rippleDrawable.draw(canvas);
}
public void setShowLabel(boolean showLabel) {
this.showLabel = showLabel;
if (showLabel)
popup = new SeekBarPopup(getContext());
}
public boolean getShowLabel() {
return showLabel;
}
public void setLabelFormat(String format) {
labelFormat = format;
}
public String getLabelFormat() {
return labelFormat;
}
public Style getStyle() {
return style;
}
public void setStyle(Style style) {
this.style = style;
}
public float getMax() {
return max;
}
public void setMax(float max) {
if (max > min) {
this.max = max;
} else {
this.max = min + step;
}
this.value = MathUtils.constrain(value, min, max);
}
public float getMin() {
return min;
}
public void setMin(float min) {
if (min < max) {
this.min = min;
} else if (this.max > step) {
this.min = max - step;
} else {
this.min = 0;
}
this.value = MathUtils.constrain(value, min, max);
}
private int stepValue(float v) {
return (int) (Math.floor((v - min + step / 2) / step) * step + min);
}
public float getValue() {
if (style == Style.Discrete)
return stepValue(value);
return value;
}
public void setValue(float value) {
if (style == Style.Discrete) {
this.value = stepValue(MathUtils.constrain(value, min, max));
} else {
this.value = MathUtils.constrain(value, min, max);
}
}
public float getStepSize() {
return step;
}
public void setStepSize(float step) {
if (step > 0) {
this.step = step;
} else {
this.step = 1;
}
}
public boolean hasTick() {
return tick;
}
public void setTick(boolean tick) {
this.tick = tick;
}
public int getTickStep() {
return tickStep;
}
public void setTickStep(int tickStep) {
this.tickStep = tickStep;
}
public int getTickColor() {
return tickColor;
}
public void setTickColor(int tickColor) {
this.tickColor = tickColor;
}
public void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) {
this.onValueChangedListener = onValueChangedListener;
}
// -------------------------------
// ripple
// -------------------------------
private RippleDrawable rippleDrawable;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (radiusAnimator != null)
radiusAnimator.end();
radiusAnimator = ValueAnimator.ofFloat(thumbRadius, THUMB_RADIUS_DRAGGED);
radiusAnimator.setDuration(200);
radiusAnimator.setInterpolator(interpolator);
radiusAnimator.addUpdateListener(animation -> {
thumbRadius = (float) animation.getAnimatedValue();
postInvalidate();
});
radiusAnimator.start();
ViewParent parent = getParent();
if (parent != null)
parent.requestDisallowInterceptTouchEvent(true);
if (showLabel)
popup.show(this);
} else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP) {
if (style == Style.Discrete) {
float val = (float) Math.floor((value - min + step / 2) / step) * step + min;
if (valueAnimator != null)
valueAnimator.cancel();
valueAnimator = ValueAnimator.ofFloat(value, val);
valueAnimator.setDuration(200);
valueAnimator.setInterpolator(interpolator);
valueAnimator.addUpdateListener(animation -> {
value = (float) animation.getAnimatedValue();
int thumbX = (int) ((value - min) / (max - min) * (getWidth() - getPaddingLeft() - getPaddingRight()) + getPaddingLeft());
int thumbY = getHeight() / 2;
int radius = rippleDrawable.getRadius();
rippleDrawable.setBounds(thumbX - radius, thumbY - radius, thumbX + radius, thumbY + radius);
postInvalidate();
});
valueAnimator.start();
}
if (radiusAnimator != null)
radiusAnimator.end();
radiusAnimator = ValueAnimator.ofFloat(thumbRadius, THUMB_RADIUS);
radiusAnimator.setDuration(200);
radiusAnimator.setInterpolator(interpolator);
radiusAnimator.addUpdateListener(animation -> {
thumbRadius = (float) animation.getAnimatedValue();
postInvalidate();
});
radiusAnimator.start();
ViewParent parent = getParent();
if (parent != null)
parent.requestDisallowInterceptTouchEvent(false);
if (showLabel)
popup.dismiss();
}
float v = (event.getX() - getPaddingLeft()) / (getWidth() - getPaddingLeft() - getPaddingRight());
v = Math.max(0, Math.min(v, 1));
float newValue = v * (max - min) + min;
int thumbX = (int) (v * (getWidth() - getPaddingLeft() - getPaddingRight()) + getPaddingLeft());
int thumbY = getHeight() / 2;
int radius = rippleDrawable.getRadius();
if (showLabel) {
int[] location = new int[2];
getLocationInWindow(location);
popup.setText(String.format(labelFormat, newValue));
popup.update(thumbX + location[0] - popup.getBubbleWidth() / 2, thumbY - radius + location[1] - popup.getHeight());
}
if (rippleDrawable != null) {
rippleDrawable.setHotspot(event.getX(), event.getY());
rippleDrawable.setBounds(thumbX - radius, thumbY - radius, thumbX + radius, thumbY + radius);
}
postInvalidate();
if (newValue != value && onValueChangedListener != null) {
if (style == Style.Discrete) {
int sv = stepValue(newValue);
if (stepValue(value) != sv)
onValueChangedListener.onValueChanged(this, sv);
} else {
onValueChangedListener.onValueChanged(this, newValue);
}
}
value = newValue;
super.onTouchEvent(event);
return true;
}
@Override
public RippleDrawable getRippleDrawable() {
return rippleDrawable;
}
public void setRippleDrawable(RippleDrawable newRipple) {
if (rippleDrawable != null) {
rippleDrawable.setCallback(null);
if (rippleDrawable.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable(rippleDrawable.getBackground());
}
if (newRipple != null) {
newRipple.setCallback(this);
newRipple.setBounds(0, 0, getWidth(), getHeight());
if (newRipple.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable((Drawable) newRipple);
}
rippleDrawable = newRipple;
}
@Override
protected boolean verifyDrawable(@NonNull Drawable who) {
return super.verifyDrawable(who) || rippleDrawable == who;
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
super.invalidateDrawable(drawable);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate();
}
@Override
public void invalidate(@NonNull Rect dirty) {
super.invalidate(dirty);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate(dirty);
}
@Override
public void invalidate(int l, int t, int r, int b) {
super.invalidate(l, t, r, b);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate(l, t, r, b);
}
@Override
public void invalidate() {
super.invalidate();
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate();
}
@Override
public void postInvalidateDelayed(long delayMilliseconds) {
super.postInvalidateDelayed(delayMilliseconds);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidateDelayed(delayMilliseconds);
}
@Override
public void postInvalidateDelayed(long delayMilliseconds, int left, int top, int right, int bottom) {
super.postInvalidateDelayed(delayMilliseconds, left, top, right, bottom);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidateDelayed(delayMilliseconds, left, top, right, bottom);
}
@Override
public void postInvalidate() {
super.postInvalidate();
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidate();
}
@Override
public void postInvalidate(int left, int top, int right, int bottom) {
super.postInvalidate(left, top, right, bottom);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidate(left, top, right, bottom);
}
@Override
public void setBackground(Drawable background) {
setBackgroundDrawable(background);
}
@Override
public void setBackgroundDrawable(Drawable background) {
if (background instanceof RippleDrawable) {
setRippleDrawable((RippleDrawable) background);
return;
}
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Background) {
rippleDrawable.setCallback(null);
rippleDrawable = null;
}
super.setBackgroundDrawable(background);
}
// -------------------------------
// state animators
// -------------------------------
private StateAnimator stateAnimator = new StateAnimator(this);
@Override
public StateAnimator getStateAnimator() {
return stateAnimator;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (rippleDrawable != null && rippleDrawable.getStyle() != RippleDrawable.Style.Background)
rippleDrawable.setState(getDrawableState());
if (stateAnimator != null)
stateAnimator.setState(getDrawableState());
}
// -------------------------------
// animations
// -------------------------------
private Animator inAnim = null, outAnim = null;
private Animator animator;
public Animator animateVisibility(final int visibility) {
if (visibility == View.VISIBLE && (getVisibility() != View.VISIBLE || animator != null)) {
if (animator != null)
animator.cancel();
if (inAnim != null) {
animator = inAnim;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator a) {
animator.removeListener(this);
animator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
animator.removeListener(this);
animator = null;
}
});
animator.start();
}
setVisibility(visibility);
} else if (visibility != View.VISIBLE && (getVisibility() == View.VISIBLE || animator != null)) {
if (animator != null)
animator.cancel();
if (outAnim == null) {
setVisibility(visibility);
return null;
}
animator = outAnim;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator a) {
if (((ValueAnimator) a).getAnimatedFraction() == 1)
setVisibility(visibility);
animator.removeListener(this);
animator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
animator.removeListener(this);
animator = null;
}
});
animator.start();
}
return animator;
}
public Animator getAnimator() {
return animator;
}
public Animator getOutAnimator() {
return outAnim;
}
public void setOutAnimator(Animator outAnim) {
if (this.outAnim != null)
this.outAnim.setTarget(null);
this.outAnim = outAnim;
if (outAnim != null)
outAnim.setTarget(this);
}
public Animator getInAnimator() {
return inAnim;
}
public void setInAnimator(Animator inAnim) {
if (this.inAnim != null)
this.inAnim.setTarget(null);
this.inAnim = inAnim;
if (inAnim != null)
inAnim.setTarget(this);
}
// -------------------------------
// tint
// -------------------------------
ColorStateList tint;
PorterDuff.Mode tintMode;
ColorStateList backgroundTint;
PorterDuff.Mode backgroundTintMode;
boolean animateColorChanges;
ValueAnimator.AnimatorUpdateListener tintAnimatorListener = animation -> {
updateTint();
ViewCompat.postInvalidateOnAnimation(SeekBar.this);
};
ValueAnimator.AnimatorUpdateListener backgroundTintAnimatorListener = animation -> {
updateBackgroundTint();
ViewCompat.postInvalidateOnAnimation(SeekBar.this);
};
@Override
public void setTint(ColorStateList list) {
this.tint = animateColorChanges && !(list instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(list, tintAnimatorListener) : list;
updateTint();
}
@Override
public void setTint(int color) {
if (color == 0) {
setTint(new DefaultPrimaryColorStateList(getContext()));
} else {
setTint(ColorStateList.valueOf(color));
}
}
@Override
public ColorStateList getTint() {
return tint;
}
private void updateTint() {
postInvalidate();
}
@Override
public void setTintMode(@NonNull PorterDuff.Mode mode) {
this.tintMode = mode;
updateTint();
}
@Override
public PorterDuff.Mode getTintMode() {
return tintMode;
}
@Override
public void setBackgroundTint(ColorStateList list) {
this.backgroundTint = animateColorChanges && !(list instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(list, backgroundTintAnimatorListener) : list;
updateBackgroundTint();
}
@Override
public void setBackgroundTint(int color) {
if (color == 0) {
setBackgroundTint(new DefaultPrimaryColorStateList(getContext()));
} else {
setBackgroundTint(ColorStateList.valueOf(color));
}
}
@Override
public ColorStateList getBackgroundTint() {
return backgroundTint;
}
private void updateBackgroundTint() {
if (getBackground() == null)
return;
if (backgroundTint != null && backgroundTintMode != null) {
int color = backgroundTint.getColorForState(getDrawableState(), backgroundTint.getDefaultColor());
getBackground().setColorFilter(new PorterDuffColorFilter(color, tintMode));
} else {
getBackground().setColorFilter(null);
}
}
@Override
public void setBackgroundTintMode(PorterDuff.Mode mode) {
this.backgroundTintMode = mode;
updateBackgroundTint();
}
@Override
public PorterDuff.Mode getBackgroundTintMode() {
return backgroundTintMode;
}
public boolean isAnimateColorChangesEnabled() {
return animateColorChanges;
}
public void setAnimateColorChangesEnabled(boolean animateColorChanges) {
this.animateColorChanges = animateColorChanges;
if (tint != null && !(tint instanceof AnimatedColorStateList))
setTint(AnimatedColorStateList.fromList(tint, tintAnimatorListener));
if (backgroundTint != null && !(backgroundTint instanceof AnimatedColorStateList))
setBackgroundTint(AnimatedColorStateList.fromList(backgroundTint, backgroundTintAnimatorListener));
}
}