背景是我们有需求,做类似ios中开关的按钮。github上有一些开源项目,比如 SwitchButton, 但是这个项目中提供了很多选项,并且实际使用中会出现一些奇怪的问题。
我调整了下代码,把无关的功能都给删了,保留核心的功能,大概这样。
package org.yeshen.widget;
// 修改自:https://github.com/zcweng/SwitchButton
// 菜单中的类似iOS中开关的样式
import static org.yeshen.widget.YsSwitchButton.ANIMATE.*;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Checkable;
public final class YsSwitchButton extends View implements Checkable {
private static final int DEFAULT_WIDTH = dp2pxInt(44);
private static final int DEFAULT_HEIGHT = dp2pxInt(25);
private static final int DEFAULT_BUTTON_PADDING = dp2pxInt(8);
private final int uncheckColor = 0xFFFF0000;
private final int checkedColor = 0xFF0000FF;
private final int uncheckButtonColor = Color.WHITE;
private float viewRadius;
private float left;
private float top;
private float right;
private float bottom;
private float centerY;
private float buttonMinX;
private float buttonMaxX;
private final Paint buttonPaint;
private final Paint paint;
private final ViewState viewState;
private final ViewState beforeState;
private final ViewState afterState;
private final RectF rect = new RectF();
private ANIMATE animateState = ANIMATE_STATE_NONE;
private final ValueAnimator valueAnimator;
private final android.animation.ArgbEvaluator argbEvaluator = new android.animation.ArgbEvaluator();
private boolean isChecked = false;
private boolean isTouchingDown = false;
private boolean isUiInit = false;
private boolean isEventBroadcast = false;
private OnCheckedChangeListener onCheckedChangeListener;
private long touchDownTime;
private boolean switchByUser;
public YsSwitchButton(Context context) {
this(context, null);
}
public YsSwitchButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public YsSwitchButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
buttonPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
buttonPaint.setColor(uncheckButtonColor);
viewState = new ViewState();
beforeState = new ViewState();
afterState = new ViewState();
valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(300);
valueAnimator.setRepeatCount(0);
ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> {
float value = (Float) animation.getAnimatedValue();
switch (animateState) {
case ANIMATE_STATE_PENDING_SETTLE: {
}
case ANIMATE_STATE_PENDING_RESET: {
}
case ANIMATE_STATE_PENDING_DRAG: {
if (animateState != ANIMATE_STATE_PENDING_DRAG) {
viewState.buttonX = beforeState.buttonX + (afterState.buttonX - beforeState.buttonX) * value;
}
viewState.checkStateColor = (int) argbEvaluator.evaluate(value, beforeState.checkStateColor, afterState.checkStateColor);
break;
}
case ANIMATE_STATE_SWITCH: {
viewState.buttonX = beforeState.buttonX + (afterState.buttonX - beforeState.buttonX) * value;
float fraction = (viewState.buttonX - buttonMinX) / (buttonMaxX - buttonMinX);
viewState.checkStateColor = (int) argbEvaluator.evaluate(fraction, uncheckColor, checkedColor);
break;
}
default:
case ANIMATE_STATE_DRAGING: {
}
case ANIMATE_STATE_NONE: {
break;
}
}
postInvalidate();
};
valueAnimator.addUpdateListener(animatorUpdateListener);
Animator.AnimatorListener animatorListener = new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
switch (animateState) {
case ANIMATE_STATE_PENDING_DRAG: {
animateState = ANIMATE_STATE_DRAGING;
postInvalidate();
break;
}
case ANIMATE_STATE_PENDING_RESET: {
animateState = ANIMATE_STATE_NONE;
postInvalidate();
break;
}
case ANIMATE_STATE_PENDING_SETTLE: {
animateState = ANIMATE_STATE_NONE;
postInvalidate();
broadcastEvent(true);
break;
}
case ANIMATE_STATE_SWITCH: {
isChecked = !isChecked;
animateState = ANIMATE_STATE_NONE;
postInvalidate();
broadcastEvent(switchByUser);
break;
}
case ANIMATE_STATE_DRAGING:
case ANIMATE_STATE_NONE:
default: {
break;
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
valueAnimator.addListener(animatorListener);
super.setClickable(true);
this.setPadding(0, 0, 0, 0);
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_WIDTH, MeasureSpec.EXACTLY);
}
if (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_HEIGHT, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
float viewPadding = 0;
float height = h - viewPadding - viewPadding;
viewRadius = height * .5f;
left = viewPadding;
top = viewPadding;
right = w - viewPadding;
bottom = h - viewPadding;
centerY = (top + bottom) * .5f;
buttonMinX = left + viewRadius;
buttonMaxX = right - viewRadius;
if (isChecked()) {
setCheckedViewState(viewState);
} else {
setUncheckViewState(viewState);
}
isUiInit = true;
postInvalidate();
}
private void setUncheckViewState(ViewState viewState) {
viewState.checkStateColor = uncheckColor;
viewState.buttonX = buttonMinX;
buttonPaint.setColor(uncheckButtonColor);
}
private void setCheckedViewState(ViewState viewState) {
viewState.checkStateColor = checkedColor;
viewState.buttonX = buttonMaxX;
int checkedButtonColor = Color.WHITE;
buttonPaint.setColor(checkedButtonColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// background color
paint.setColor(uncheckColor);
drawRoundRect(canvas, left, top, right, bottom, viewRadius, paint);
// select color
paint.setColor(viewState.checkStateColor);
drawRoundRect(canvas, left, top, right, bottom, viewRadius, paint);
// button
canvas.drawCircle(viewState.buttonX, centerY,
viewRadius - DEFAULT_BUTTON_PADDING / 2F, buttonPaint);
}
@SuppressLint("ObsoleteSdkInt")
private void drawRoundRect(Canvas canvas, float left, float top, float right,
float bottom, float backgroundRadius, Paint paint) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas.drawRoundRect(left, top, right, bottom, backgroundRadius, backgroundRadius, paint);
} else {
rect.set(left, top, right, bottom);
canvas.drawRoundRect(rect, backgroundRadius, backgroundRadius, paint);
}
}
@Override
public void setChecked(boolean checked) {
if (checked == isChecked()) {
postInvalidate();
return;
}
toggle(true, false);
}
@Override
public boolean isChecked() {
return isChecked;
}
@Override
public void toggle() {
toggle(true);
}
public void toggle(boolean animate) {
toggle(animate, true);
}
private void toggle(boolean animate, boolean broadcast) {
toggle(animate, broadcast, false);
}
private void toggle(boolean animate, boolean broadcast, boolean byUser) {
if (!isEnabled()) {
return;
}
if (isEventBroadcast) {
throw new RuntimeException("should NOT switch the state in method: [onCheckedChanged]!");
}
if (!isUiInit) {
isChecked = !isChecked;
if (broadcast) {
broadcastEvent(byUser);
}
return;
}
if (valueAnimator.isRunning()) {
valueAnimator.cancel();
}
if (!animate) {
isChecked = !isChecked;
if (isChecked()) {
setCheckedViewState(viewState);
} else {
setUncheckViewState(viewState);
}
postInvalidate();
if (broadcast) {
broadcastEvent(byUser);
}
return;
}
animateState = ANIMATE_STATE_SWITCH;
switchByUser = byUser;
beforeState.copy(viewState);
if (isChecked()) {
setUncheckViewState(afterState);
} else {
setCheckedViewState(afterState);
}
valueAnimator.start();
}
private void broadcastEvent(boolean byUser) {
if (onCheckedChangeListener != null) {
isEventBroadcast = true;
onCheckedChangeListener.onCheckedChanged(this, isChecked(), byUser);
}
isEventBroadcast = false;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
int actionMasked = event.getActionMasked();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
isTouchingDown = true;
touchDownTime = System.currentTimeMillis();
removeCallbacks(postPendingDrag);
postDelayed(postPendingDrag, 100);
break;
}
case MotionEvent.ACTION_MOVE: {
float eventX = event.getX();
if (isPendingDragState()) {
float fraction = eventX / getWidth();
fraction = Math.max(0f, Math.min(1f, fraction));
viewState.buttonX = buttonMinX + (buttonMaxX - buttonMinX) * fraction;
} else if (isDragState()) {
float fraction = eventX / getWidth();
fraction = Math.max(0f, Math.min(1f, fraction));
viewState.buttonX = buttonMinX + (buttonMaxX - buttonMinX) * fraction;
viewState.checkStateColor = (int) argbEvaluator.evaluate(fraction, uncheckColor, checkedColor);
postInvalidate();
}
break;
}
case MotionEvent.ACTION_UP: {
isTouchingDown = false;
removeCallbacks(postPendingDrag);
if (System.currentTimeMillis() - touchDownTime <= 300) {
toggle(true, true, true);
} else if (isDragState()) {
float eventX = event.getX();
float fraction = eventX / getWidth();
fraction = Math.max(0f, Math.min(1f, fraction));
boolean newCheck = fraction > .5f;
if (newCheck == isChecked()) {
pendingCancelDragState();
} else {
isChecked = newCheck;
pendingSettleState();
}
} else if (isPendingDragState()) {
pendingCancelDragState();
}
break;
}
case MotionEvent.ACTION_CANCEL: {
isTouchingDown = false;
removeCallbacks(postPendingDrag);
if (isPendingDragState() || isDragState()) {
pendingCancelDragState();
}
break;
}
}
return true;
}
private final Runnable postPendingDrag = () -> {
if (!isInAnimating()) {
pendingDragState();
}
};
private boolean isInAnimating() {
return animateState != ANIMATE_STATE_NONE;
}
private boolean isPendingDragState() {
return animateState == ANIMATE_STATE_PENDING_DRAG || animateState == ANIMATE_STATE_PENDING_RESET;
}
private boolean isDragState() {
return animateState == ANIMATE_STATE_DRAGING;
}
private void pendingDragState() {
if (isInAnimating()) {
return;
}
if (!isTouchingDown) {
return;
}
if (valueAnimator.isRunning()) {
valueAnimator.cancel();
}
animateState = ANIMATE_STATE_PENDING_DRAG;
beforeState.copy(viewState);
afterState.copy(viewState);
if (isChecked()) {
afterState.checkStateColor = checkedColor;
afterState.buttonX = buttonMaxX;
} else {
afterState.checkStateColor = uncheckColor;
afterState.buttonX = buttonMinX;
}
valueAnimator.start();
}
private void pendingCancelDragState() {
if (isDragState() || isPendingDragState()) {
if (valueAnimator.isRunning()) {
valueAnimator.cancel();
}
animateState = ANIMATE_STATE_PENDING_RESET;
beforeState.copy(viewState);
if (isChecked()) {
setCheckedViewState(afterState);
} else {
setUncheckViewState(afterState);
}
valueAnimator.start();
}
}
private void pendingSettleState() {
if (valueAnimator.isRunning()) {
valueAnimator.cancel();
}
animateState = ANIMATE_STATE_PENDING_SETTLE;
beforeState.copy(viewState);
if (isChecked()) {
setCheckedViewState(afterState);
} else {
setUncheckViewState(afterState);
}
valueAnimator.start();
}
@SuppressWarnings("unused")
public void setOnCheckedChangeListener(OnCheckedChangeListener l) {
onCheckedChangeListener = l;
}
public interface OnCheckedChangeListener {
void onCheckedChanged(YsSwitchButton view, boolean isChecked, boolean byUser);
}
private static float dp2px(float dp) {
Resources r = Resources.getSystem();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
}
private static int dp2pxInt(float dp) {
return (int) dp2px(dp);
}
private static class ViewState {
float buttonX;
int checkStateColor;
private void copy(ViewState source) {
this.buttonX = source.buttonX;
this.checkStateColor = source.checkStateColor;
}
}
enum ANIMATE {
ANIMATE_STATE_NONE, ANIMATE_STATE_PENDING_DRAG, ANIMATE_STATE_DRAGING, ANIMATE_STATE_PENDING_RESET, ANIMATE_STATE_PENDING_SETTLE, ANIMATE_STATE_SWITCH;
}
}