package com.android.app_base.widget; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RadialGradient; import android.graphics.RectF; import android.graphics.Shader; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.animation.AccelerateInterpolator; import androidx.annotation.Nullable; import com.android.app_base.R; import com.android.app_base.utils.ScreenSizeUtils; /** * 高仿 ios 开关按钮 */ public final class SwitchButton extends View { private static final int STATE_SWITCH_OFF = 1; private static final int STATE_SWITCH_OFF2 = 2; private static final int STATE_SWITCH_ON = 3; private static final int STATE_SWITCH_ON2 = 4; private final AccelerateInterpolator mInterpolator = new AccelerateInterpolator(2); private final Paint mPaint = new Paint(); private final Path mBackgroundPath = new Path(); private final Path mBarPath = new Path(); private final RectF mBound = new RectF(); private float mAnim1, mAnim2; private RadialGradient mShadowGradient; /** 按钮宽高形状比率(0,1] 不推荐大幅度调整 */ protected final float mAspectRatio = 0.68f; /** (0,1] */ protected final float mAnimationSpeed = 0.1f; /** 上一个选中状态 */ private int mLastCheckedState; /** 当前的选中状态 */ private int mCheckedState; private boolean mCanVisibleDrawing = false; /** 是否显示按钮阴影 */ protected boolean mShadow; /** 是否选中 */ protected boolean mChecked; /** 开启状态背景色 */ protected int mAccentColor = 0xFF4BD763; /** 开启状态按钮描边色 */ protected int mPrimaryDarkColor = 0xFF3AC652; /** 关闭状态描边色 */ protected int mOffColor = 0xFFE3E3E3; /** 关闭状态按钮描边色 */ protected int mOffDarkColor = 0xFFBFBFBF; /** 按钮阴影色 */ protected int mShadowColor = 0xFF333333; /** 监听器 */ @Nullable private OnCheckedChangeListener mListener; private float mRight; private float mCenterX, mCenterY; private float mScale; private float mOffset; private float mRadius, mStrokeWidth; private float mWidth; private float mLeft; private float bRight; private float mOnLeftX, mOn2LeftX, mOff2LeftX, mOffLeftX; private float mShadowReservedHeight; public SwitchButton(Context context) { this(context, null); } public SwitchButton(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setLayerType(LAYER_TYPE_SOFTWARE, null); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton); mChecked = array.getBoolean(R.styleable.SwitchButton_android_checked, mChecked); setEnabled(array.getBoolean(R.styleable.SwitchButton_android_enabled, isEnabled())); mLastCheckedState = mCheckedState = mChecked ? STATE_SWITCH_ON : STATE_SWITCH_OFF; array.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { switch (MeasureSpec.getMode(widthMeasureSpec)) { case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (ScreenSizeUtils.dip2px(56) + getPaddingLeft() + getPaddingRight()), MeasureSpec.EXACTLY); break; case MeasureSpec.EXACTLY: default: break; } switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (MeasureSpec.getSize(widthMeasureSpec) * mAspectRatio) + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY); break; case MeasureSpec.EXACTLY: default: break; } setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); } @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { mCanVisibleDrawing = width > getPaddingLeft() + getPaddingRight() && height > getPaddingTop() + getPaddingBottom(); if (mCanVisibleDrawing) { int actuallyDrawingAreaWidth = width - getPaddingLeft() - getPaddingRight(); int actuallyDrawingAreaHeight = height - getPaddingTop() - getPaddingBottom(); int actuallyDrawingAreaLeft; int actuallyDrawingAreaRight; int actuallyDrawingAreaTop; int actuallyDrawingAreaBottom; if (actuallyDrawingAreaWidth * mAspectRatio < actuallyDrawingAreaHeight) { actuallyDrawingAreaLeft = getPaddingLeft(); actuallyDrawingAreaRight = width - getPaddingRight(); int heightExtraSize = (int) (actuallyDrawingAreaHeight - actuallyDrawingAreaWidth * mAspectRatio); actuallyDrawingAreaTop = getPaddingTop() + heightExtraSize / 2; actuallyDrawingAreaBottom = getHeight() - getPaddingBottom() - heightExtraSize / 2; } else { int widthExtraSize = (int) (actuallyDrawingAreaWidth - actuallyDrawingAreaHeight / mAspectRatio); actuallyDrawingAreaLeft = getPaddingLeft() + widthExtraSize / 2; actuallyDrawingAreaRight = getWidth() - getPaddingRight() - widthExtraSize / 2; actuallyDrawingAreaTop = getPaddingTop(); actuallyDrawingAreaBottom = getHeight() - getPaddingBottom(); } mShadowReservedHeight = (int) ((actuallyDrawingAreaBottom - actuallyDrawingAreaTop) * 0.07f); float left = actuallyDrawingAreaLeft; float top = actuallyDrawingAreaTop + mShadowReservedHeight; mRight = actuallyDrawingAreaRight; float bottom = actuallyDrawingAreaBottom - mShadowReservedHeight; float sHeight = bottom - top; mCenterX = (mRight + left) / 2; mCenterY = (bottom + top) / 2; mLeft = left; mWidth = bottom - top; bRight = left + mWidth; // OfB final float halfHeightOfS = mWidth / 2; mRadius = halfHeightOfS * 0.95f; // offset of switching mOffset = mRadius * 0.2f; mStrokeWidth = (halfHeightOfS - mRadius) * 2; mOnLeftX = mRight - mWidth; mOn2LeftX = mOnLeftX - mOffset; mOffLeftX = left; mOff2LeftX = mOffLeftX + mOffset; mScale = 1 - mStrokeWidth / sHeight; mBackgroundPath.reset(); RectF bound = new RectF(); bound.top = top; bound.bottom = bottom; bound.left = left; bound.right = left + sHeight; mBackgroundPath.arcTo(bound, 90, 180); bound.left = mRight - sHeight; bound.right = mRight; mBackgroundPath.arcTo(bound, 270, 180); mBackgroundPath.close(); mBound.left = mLeft; mBound.right = bRight; // bTop = sTop mBound.top = top + mStrokeWidth / 2; // bBottom = sBottom mBound.bottom = bottom - mStrokeWidth / 2; float bCenterX = (bRight + mLeft) / 2; float bCenterY = (bottom + top) / 2; int red = mShadowColor >> 16 & 0xFF; int green = mShadowColor >> 8 & 0xFF; int blue = mShadowColor & 0xFF; mShadowGradient = new RadialGradient(bCenterX, bCenterY, mRadius, Color.argb(200, red, green, blue), Color.argb(25, red, green, blue), Shader.TileMode.CLAMP); } } private void calcBPath(float percent) { mBarPath.reset(); mBound.left = mLeft + mStrokeWidth / 2; mBound.right = bRight - mStrokeWidth / 2; mBarPath.arcTo(mBound, 90, 180); mBound.left = mLeft + percent * mOffset + mStrokeWidth / 2; mBound.right = bRight + percent * mOffset - mStrokeWidth / 2; mBarPath.arcTo(mBound, 270, 180); mBarPath.close(); } private float calcBTranslate(float percent) { float result = 0; switch (mCheckedState - mLastCheckedState) { case 1: if (mCheckedState == STATE_SWITCH_OFF2) { // off -> off2 result = mOffLeftX; } else if (mCheckedState == STATE_SWITCH_ON) { // on2 -> on result = mOnLeftX - (mOnLeftX - mOn2LeftX) * percent; } break; case 2: if (mCheckedState == STATE_SWITCH_ON) { // off2 -> on result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent; } else if (mCheckedState == STATE_SWITCH_ON2) { // off -> on2 result = mOn2LeftX - (mOn2LeftX - mOffLeftX) * percent; } break; case 3: // off -> on result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent; break; case -1: if (mCheckedState == STATE_SWITCH_ON2) { // on -> on2 result = mOn2LeftX + (mOnLeftX - mOn2LeftX) * percent; } else if (mCheckedState == STATE_SWITCH_OFF) { // off2 -> off result = mOffLeftX; } break; case -2: if (mCheckedState == STATE_SWITCH_OFF) { // on2 -> off result = mOffLeftX + (mOn2LeftX - mOffLeftX) * percent; } else if (mCheckedState == STATE_SWITCH_OFF2) { // on -> off2 result = mOff2LeftX + (mOnLeftX - mOff2LeftX) * percent; } break; case -3: // on -> off result = mOffLeftX + (mOnLeftX - mOffLeftX) * percent; break; default: // init case 0: if (mCheckedState == STATE_SWITCH_OFF) { // off -> off result = mOffLeftX; } else if (mCheckedState == STATE_SWITCH_ON) { // on -> on result = mOnLeftX; } break; } return result - mOffLeftX; } @Override protected void onDraw(Canvas canvas) { if (!mCanVisibleDrawing) { return; } mPaint.setAntiAlias(true); final boolean isOn = (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_ON2); // Draw background mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(isOn ? mAccentColor : mOffColor); canvas.drawPath(mBackgroundPath, mPaint); mAnim1 = mAnim1 - mAnimationSpeed > 0 ? mAnim1 - mAnimationSpeed : 0; mAnim2 = mAnim2 - mAnimationSpeed > 0 ? mAnim2 - mAnimationSpeed : 0; final float dsAnim = mInterpolator.getInterpolation(mAnim1); final float dbAnim = mInterpolator.getInterpolation(mAnim2); // Draw background animation final float scale = mScale * (isOn ? dsAnim : 1 - dsAnim); final float scaleOffset = (mRight - mCenterX - mRadius) * (isOn ? 1 - dsAnim : dsAnim); canvas.save(); canvas.scale(scale, scale, mCenterX + scaleOffset, mCenterY); if (isEnabled()) { mPaint.setColor(0xFFFFFFFF); } else { mPaint.setColor(0xFFBBBBBB); } canvas.drawPath(mBackgroundPath, mPaint); canvas.restore(); // To prepare center bar path canvas.save(); canvas.translate(calcBTranslate(dbAnim), mShadowReservedHeight); final boolean isState2 = (mCheckedState == STATE_SWITCH_ON2 || mCheckedState == STATE_SWITCH_OFF2); calcBPath(isState2 ? 1 - dbAnim : dbAnim); // Use center bar path to draw shadow if (mShadow) { mPaint.setStyle(Paint.Style.FILL); mPaint.setShader(mShadowGradient); canvas.drawPath(mBarPath, mPaint); mPaint.setShader(null); } canvas.translate(0, -mShadowReservedHeight); // draw bar canvas.scale(0.98f, 0.98f, mWidth / 2, mWidth / 2); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(0xFFFFFFFF); canvas.drawPath(mBarPath, mPaint); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mStrokeWidth * 0.5f); mPaint.setColor(isOn ? mPrimaryDarkColor : mOffDarkColor); canvas.drawPath(mBarPath, mPaint); canvas.restore(); mPaint.reset(); if (mAnim1 > 0 || mAnim2 > 0) { invalidate(); } } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); if (isEnabled() && (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_OFF) && (mAnim1 * mAnim2 == 0)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: mLastCheckedState = mCheckedState; mAnim2 = 1; switch (mCheckedState) { case STATE_SWITCH_OFF: setChecked(true, false); if (mListener != null) { mListener.onCheckedChanged(this, true); } break; case STATE_SWITCH_ON: setChecked(false, false); if (mListener != null) { mListener.onCheckedChanged(this, false); } break; default: break; } break; case MotionEvent.ACTION_DOWN: default: break; } } return true; } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState state = new SavedState(superState); state.checked = mChecked; return state; } @Override public void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); mChecked = savedState.checked; mCheckedState = mChecked ? STATE_SWITCH_ON : STATE_SWITCH_OFF; invalidate(); } public void setColor(int newColorPrimary, int newColorPrimaryDark) { setColor(newColorPrimary, newColorPrimaryDark, mOffColor, mOffDarkColor); } public void setColor(int newColorPrimary, int newColorPrimaryDark, int newColorOff, int newColorOffDark) { setColor(newColorPrimary, newColorPrimaryDark, newColorOff, newColorOffDark, mShadowColor); } public void setColor(int newColorPrimary, int newColorPrimaryDark, int newColorOff, int newColorOffDark, int newColorShadow) { mAccentColor = newColorPrimary; mPrimaryDarkColor = newColorPrimaryDark; mOffColor = newColorOff; mOffDarkColor = newColorOffDark; mShadowColor = newColorShadow; invalidate(); } /** * 设置按钮阴影开关 */ public void setShadow(boolean shadow) { mShadow = shadow; invalidate(); } /** * 当前状态是否选中 */ public boolean isChecked() { return mChecked; } /** * 设置选择状态(默认会回调监听器) */ public void setChecked(boolean checked) { // 回调监听器 setChecked(checked, true); } /** * 设置选择状态 */ public void setChecked(boolean checked, boolean callback) { int newState = checked ? STATE_SWITCH_ON : STATE_SWITCH_OFF; if (newState == mCheckedState) { return; } if ((newState == STATE_SWITCH_ON && (mCheckedState == STATE_SWITCH_OFF || mCheckedState == STATE_SWITCH_OFF2)) || (newState == STATE_SWITCH_OFF && (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_ON2))) { mAnim1 = 1; } mAnim2 = 1; if (!mChecked && newState == STATE_SWITCH_ON) { mChecked = true; } else if (mChecked && newState == STATE_SWITCH_OFF) { mChecked = false; } mLastCheckedState = mCheckedState; mCheckedState = newState; postInvalidate(); if (callback && mListener != null) { mListener.onCheckedChanged(this, checked); } } /** * 设置选中状态改变监听 */ public void setOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener) { mListener = listener; } /** * 选中监听器 */ public interface OnCheckedChangeListener { /** * 回调监听 * * @param button 切换按钮 * @param checked 是否选中 */ void onCheckedChanged(SwitchButton button, boolean checked); } /** * 保存开关状态 */ private static final class SavedState extends BaseSavedState { private boolean checked; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); checked = 1 == in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(checked ? 1 : 0); } /** * fixed by Night99 https://github.com/g19980115 */ @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }