package com.marshalchen.common.uimodule.switchbutton;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import android.widget.CompoundButton;
import com.marshalchen.common.uimodule.animation.R;
import com.marshalchen.common.uimodule.switchbutton.AnimationController.OnAnimateListener;
/**
* SwitchButton widget which is easy to use
*
* @version 1.2
* @author kyleduo
* @since 2014-09-24
*/
public class SwitchButton extends CompoundButton {
private static boolean SHOW_RECT = false;
private boolean mIsChecked = false;
private Configuration mConf;
/**
* zone for thumb to move inside
*/
private Rect mSafeZone;
/**
* zone for background
*/
private Rect mBackZone;
private Rect mThumbZone;
private RectF mSaveLayerZone;
private AnimationController mAnimationController;
private SBAnimationListener mOnAnimateListener = new SBAnimationListener();
private boolean isAnimating = false;
private float mStartX, mStartY, mLastX;
private float mCenterPos;
private int mTouchSlop;
private int mClickTimeout;
private Paint mRectPaint;
private Rect mBounds = null;
private OnCheckedChangeListener mOnCheckedChangeListener;
public SwitchButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton);
mConf.setThumbMarginInPixel(ta.getDimensionPixelSize(R.styleable.SwitchButton_thumb_margin, mConf.getDefaultThumbMarginInPixel()));
mConf.setThumbMarginInPixel(ta.getDimensionPixelSize(R.styleable.SwitchButton_thumb_marginTop, mConf.getThumbMarginTop()),
ta.getDimensionPixelSize(R.styleable.SwitchButton_thumb_marginBottom, mConf.getThumbMarginBottom()),
ta.getDimensionPixelSize(R.styleable.SwitchButton_thumb_marginLeft, mConf.getThumbMarginLeft()),
ta.getDimensionPixelSize(R.styleable.SwitchButton_thumb_marginRight, mConf.getThumbMarginRight()));
mConf.setRadius(ta.getInt(R.styleable.SwitchButton_switch_button_radius, Configuration.Default.DEFAULT_RADIUS));
mConf.setThumbWidthAndHeightInPixel(ta.getDimensionPixelSize(R.styleable.SwitchButton_thumb_width, -1), ta.getDimensionPixelSize(R.styleable.SwitchButton_thumb_height, -1));
mConf.setMeasureFactor(ta.getFloat(R.styleable.SwitchButton_measureFactor, -1));
mConf.setInsetBounds(ta.getDimensionPixelSize(R.styleable.SwitchButton_insetLeft, 0), ta.getDimensionPixelSize(R.styleable.SwitchButton_insetTop, 0),
ta.getDimensionPixelSize(R.styleable.SwitchButton_insetRight, 0), ta.getDimensionPixelSize(R.styleable.SwitchButton_insetBottom, 0));
int velocity = ta.getInteger(R.styleable.SwitchButton_animationVelocity, -1);
mAnimationController.setVelocity(velocity);
fetchDrawableFromAttr(ta);
ta.recycle();
}
public SwitchButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwitchButton(Context context) {
this(context, null);
}
private void initView() {
mConf = Configuration.getDefault(getContext().getResources().getDisplayMetrics().density);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
mAnimationController = AnimationController.getDefault().init(mOnAnimateListener);
mBounds = new Rect();
if (SHOW_RECT) {
mRectPaint = new Paint();
mRectPaint.setStyle(Style.STROKE);
}
}
/**
* fetch drawable resources from attrs, drop them to conf, AFTER the size
* has been confirmed
*
* @param ta
*/
private void fetchDrawableFromAttr(TypedArray ta) {
if (mConf == null) {
return;
}
mConf.setOffDrawable(fetchDrawable(ta, R.styleable.SwitchButton_switch_button_offDrawable, R.styleable.SwitchButton_switch_button_offColor, Configuration.Default.DEFAULT_OFF_COLOR));
mConf.setOnDrawable(fetchDrawable(ta, R.styleable.SwitchButton_switch_button_onDrawable, R.styleable.SwitchButton_switch_button_onColor, Configuration.Default.DEFAULT_ON_COLOR));
mConf.setThumbDrawable(fetchDrawable(ta, R.styleable.SwitchButton_thumbDrawable, R.styleable.SwitchButton_thumbColor, Configuration.Default.DEFAULT_THUMB_COLOR));
}
private Drawable fetchDrawable(TypedArray ta, int attrId, int alterColorId, int defaultColor) {
Drawable tempDrawable = ta.getDrawable(attrId);
if (tempDrawable == null) {
int tempColor = ta.getColor(alterColorId, defaultColor);
tempDrawable = new GradientDrawable();
((GradientDrawable) tempDrawable).setCornerRadius(this.mConf.getRadius());
((GradientDrawable) tempDrawable).setColor(tempColor);
}
return tempDrawable;
}
public void setConfiguration(Configuration conf) {
if (mConf == null) {
mConf = Configuration.getDefault(conf.getDensity());
}
mConf.setOffDrawable(conf.getOffDrawableWithFix());
mConf.setOnDrawable(conf.getOnDrawableWithFix());
mConf.setThumbDrawable(conf.getThumbDrawableWithFix());
mConf.setThumbMarginInPixel(conf.getThumbMarginTop(), conf.getThumbMarginBottom(), conf.getThumbMarginLeft(), conf.getThumbMarginRight());
mConf.setThumbWidthAndHeightInPixel(conf.getThumbWidth(), conf.getThumbHeight());
mConf.setVelocity(conf.getVelocity());
mConf.setMeasureFactor(conf.getMeasureFactor());
mAnimationController.setVelocity(mConf.getVelocity());
this.requestLayout();
setup();
setChecked(mIsChecked);
}
/**
* return a REFERENCE of configuration, it is suggested that not to change that
*
* @return
*/
public Configuration getConfiguration() {
return mConf;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
setup();
}
private void setup() {
setupBackZone();
setupSafeZone();
setupThumbZone();
setupDrawableBounds();
if (this.getMeasuredWidth() > 0 && this.getMeasuredHeight() > 0) {
mSaveLayerZone = new RectF(0, 0, this.getMeasuredWidth(), this.getMeasuredHeight());
}
}
/**
* setup zone for thumb to move
*
*/
private void setupSafeZone() {
int w = getMeasuredWidth();
int h = getMeasuredHeight();
if (w > 0 && h > 0) {
if (mSafeZone == null) {
mSafeZone = new Rect();
}
int left, right, top, bottom;
left = getPaddingLeft() + (mConf.getThumbMarginLeft() > 0 ? mConf.getThumbMarginLeft() : 0);
right = w - getPaddingRight() - (mConf.getThumbMarginRight() > 0 ? mConf.getThumbMarginRight() : 0) + (-mConf.getShrinkX());
top = getPaddingTop() + (mConf.getThumbMarginTop() > 0 ? mConf.getThumbMarginTop() : 0);
bottom = h - getPaddingBottom() - (mConf.getThumbMarginBottom() > 0 ? mConf.getThumbMarginBottom() : 0) + (-mConf.getShrinkY());
mSafeZone.set(left, top, right, bottom);
mCenterPos = mSafeZone.left + (mSafeZone.right - mSafeZone.left - mConf.getThumbWidth()) / 2;
} else {
mSafeZone = null;
}
}
private void setupBackZone() {
int w = getMeasuredWidth();
int h = getMeasuredHeight();
if (w > 0 && h > 0) {
if (mBackZone == null) {
mBackZone = new Rect();
}
int left, right, top, bottom;
left = getPaddingLeft() + (mConf.getThumbMarginLeft() > 0 ? 0 : -mConf.getThumbMarginLeft());
right = w - getPaddingRight() - (mConf.getThumbMarginRight() > 0 ? 0 : -mConf.getThumbMarginRight()) + (-mConf.getShrinkX());
top = getPaddingTop() + (mConf.getThumbMarginTop() > 0 ? 0 : -mConf.getThumbMarginTop());
bottom = h - getPaddingBottom() - (mConf.getThumbMarginBottom() > 0 ? 0 : -mConf.getThumbMarginBottom()) + (-mConf.getShrinkY());
mBackZone.set(left, top, right, bottom);
} else {
mBackZone = null;
}
}
private void setupThumbZone() {
int w = getMeasuredWidth();
int h = getMeasuredHeight();
if (w > 0 && h > 0) {
if (mThumbZone == null) {
mThumbZone = new Rect();
}
int left, right, top, bottom;
left = mIsChecked ? (mSafeZone.right - mConf.getThumbWidth()) : mSafeZone.left;
right = left + mConf.getThumbWidth();
top = mSafeZone.top;
bottom = top + mConf.getThumbHeight();
mThumbZone.set(left, top, right, bottom);
} else {
mThumbZone = null;
}
}
private void setupDrawableBounds() {
if (mBackZone != null) {
mConf.getOnDrawable().setBounds(mBackZone);
mConf.getOffDrawable().setBounds(mBackZone);
}
if (mThumbZone != null) {
mConf.getThumbDrawable().setBounds(mThumbZone);
}
}
private int measureWidth(int measureSpec) {
int measuredWidth = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
int minWidth = (int) (mConf.getThumbWidth() * mConf.getMeasureFactor() + getPaddingLeft() + getPaddingRight());
int innerMarginWidth = mConf.getThumbMarginLeft() + mConf.getThumbMarginRight();
if (innerMarginWidth > 0) {
minWidth += innerMarginWidth;
}
if (specMode == MeasureSpec.EXACTLY) {
measuredWidth = Math.max(specSize, minWidth);
} else {
measuredWidth = minWidth;
if (specMode == MeasureSpec.AT_MOST) {
measuredWidth = Math.min(specSize, minWidth);
}
}
// bounds are negative numbers
measuredWidth += (mConf.getInsetBounds().left + mConf.getInsetBounds().right);
return measuredWidth;
}
private int measureHeight(int measureSpec) {
int measuredHeight = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
int minHeight = mConf.getThumbHeight() + getPaddingTop() + getPaddingBottom();
int innerMarginHeight = mConf.getThumbMarginTop() + mConf.getThumbMarginBottom();
if (innerMarginHeight > 0) {
minHeight += innerMarginHeight;
}
if (specMode == MeasureSpec.EXACTLY) {
measuredHeight = Math.max(specSize, minHeight);
} else {
measuredHeight = minHeight;
if (specMode == MeasureSpec.AT_MOST) {
measuredHeight = Math.min(specSize, minHeight);
}
}
measuredHeight += (mConf.getInsetBounds().top + mConf.getInsetBounds().bottom);
return measuredHeight;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.getClipBounds(mBounds);
if (mBounds != null && mConf.needShrink()) {
mBounds.inset(mConf.getInsetX(), mConf.getInsetY());
canvas.clipRect(mBounds, Region.Op.REPLACE);
canvas.translate(mConf.getInsetBounds().left, mConf.getInsetBounds().top);
}
boolean useGeneralDisableEffect = !isEnabled() && this.notStatableDrawable();
if (useGeneralDisableEffect) {
canvas.saveLayerAlpha(mSaveLayerZone, 255 / 2, Canvas.MATRIX_SAVE_FLAG | Canvas.CLIP_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.FULL_COLOR_LAYER_SAVE_FLAG
| Canvas.CLIP_TO_LAYER_SAVE_FLAG);
}
mConf.getOffDrawable().draw(canvas);
mConf.getOnDrawable().setAlpha(calcAlpha());
mConf.getOnDrawable().draw(canvas);
mConf.getThumbDrawable().draw(canvas);
if (useGeneralDisableEffect) {
canvas.restore();
}
if (SHOW_RECT) {
mRectPaint.setColor(Color.parseColor("#AA0000"));
canvas.drawRect(mBackZone, mRectPaint);
mRectPaint.setColor(Color.parseColor("#00FF00"));
canvas.drawRect(mSafeZone, mRectPaint);
mRectPaint.setColor(Color.parseColor("#0000FF"));
canvas.drawRect(mThumbZone, mRectPaint);
}
}
private boolean notStatableDrawable() {
boolean thumbStatable = (mConf.getThumbDrawable() instanceof StateListDrawable);
boolean onStatable = (mConf.getOnDrawable() instanceof StateListDrawable);
boolean offStatable = (mConf.getOffDrawable() instanceof StateListDrawable);
return !thumbStatable || !onStatable || !offStatable;
}
/**
* calculate the alpha value for on layer
* @return 0 ~ 255
*/
private int calcAlpha() {
int alpha = 255;
if (mSafeZone == null || mSafeZone.right == mSafeZone.left) {
} else {
int backWidth = mSafeZone.right - mConf.getThumbWidth() - mSafeZone.left;
if (backWidth > 0) {
alpha = (mThumbZone.left - mSafeZone.left) * 255 / backWidth;
}
}
return alpha;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isAnimating || !isEnabled()) {
return false;
}
int action = event.getAction();
float deltaX = event.getX() - mStartX;
float deltaY = event.getY() - mStartY;
// status the view going to change to when finger released
boolean nextStatus = mIsChecked;
switch (action) {
case MotionEvent.ACTION_DOWN:
catchView();
mStartX = event.getX();
mStartY = event.getY();
mLastX = mStartX;
setPressed(true);
break;
case MotionEvent.ACTION_MOVE:
float x = event.getX();
moveThumb((int) (x - mLastX));
mLastX = x;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
setPressed(false);
nextStatus = getStatusBasedOnPos();
float time = event.getEventTime() - event.getDownTime();
if (deltaX < mTouchSlop && deltaY < mTouchSlop && time < mClickTimeout) {
performClick();
} else {
slideToChecked(nextStatus);
}
break;
default:
break;
}
invalidate();
return true;
}
/**
* return the status based on position of thumb
* @return
*/
private boolean getStatusBasedOnPos() {
return mThumbZone.left > mCenterPos;
}
@Override
public boolean performClick() {
return super.performClick();
}
private void catchView() {
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
@Override
public void setChecked(final boolean checked) {
if (mThumbZone != null) {
moveThumb(checked ? getMeasuredWidth() : -getMeasuredWidth());
}
setCheckedInClass(checked);
}
@Override
public boolean isChecked() {
return mIsChecked;
}
@Override
public void toggle() {
toggle(true);
}
public void toggle(boolean animated) {
if (animated) {
slideToChecked(!mIsChecked);
} else {
setChecked(!mIsChecked);
}
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
setDrawableState(mConf.getThumbDrawable());
setDrawableState(mConf.getOnDrawable());
setDrawableState(mConf.getOffDrawable());
}
private void setDrawableState(Drawable drawable) {
if (drawable != null) {
int[] myDrawableState = getDrawableState();
drawable.setState(myDrawableState);
invalidate();
}
}
public void setOnCheckedChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
if (onCheckedChangeListener == null) {
throw new IllegalArgumentException("onCheckedChangeListener can not be null");
}
mOnCheckedChangeListener = onCheckedChangeListener;
}
private void setCheckedInClass(boolean checked) {
if (mIsChecked == checked) {
return;
}
mIsChecked = checked;
refreshDrawableState();
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, mIsChecked);
}
}
public void slideToChecked(boolean checked) {
if (isAnimating) {
return;
}
int from = mThumbZone.left;
int to = checked ? mSafeZone.right - mConf.getThumbWidth() : mSafeZone.left;
mAnimationController.startAnimation(from, to);
}
private void moveThumb(int delta) {
int newLeft = mThumbZone.left + delta;
int newRight = mThumbZone.right + delta;
if (newLeft < mSafeZone.left) {
newLeft = mSafeZone.left;
newRight = newLeft + mConf.getThumbWidth();
}
if (newRight > mSafeZone.right) {
newRight = mSafeZone.right;
newLeft = newRight - mConf.getThumbWidth();
}
moveThumbTo(newLeft, newRight);
}
private void moveThumbTo(int newLeft, int newRight) {
mThumbZone.set(newLeft, mThumbZone.top, newRight, mThumbZone.bottom);
mConf.getThumbDrawable().setBounds(mThumbZone);
}
class SBAnimationListener implements OnAnimateListener {
@Override
public void onAnimationStart() {
isAnimating = true;
}
@Override
public boolean continueAnimating() {
return mThumbZone.right < mSafeZone.right && mThumbZone.left > mSafeZone.left;
}
@Override
public void onFrameUpdate(int frame) {
moveThumb(frame);
postInvalidate();
}
@Override
public void onAnimateComplete() {
setCheckedInClass(getStatusBasedOnPos());
isAnimating = false;
}
}
}