| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- package com.hjq.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.Build;
- import android.os.Parcel;
- import android.os.Parcelable;
- import android.support.annotation.Nullable;
- import android.support.annotation.RequiresApi;
- import android.util.AttributeSet;
- import android.util.TypedValue;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.animation.AccelerateInterpolator;
- /**
- * desc : 开关按钮
- */
- 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;
- protected float mAspectRatio = 0.68f; // 按钮宽高形状比率(0,1] 不推荐大幅度调整
- protected float mAnimationSpeed = 0.1f; // (0,1]
- private int mLastCheckedState; // 上一个选中状态
- private int mCheckedState; // 当前的选中状态
- private boolean isCanVisibleDrawing = false;
- protected boolean isShadow; // 是否显示按钮阴影
- protected boolean mChecked; // 是否选中
- protected int mAccentColor = 0xFF4BD763; // 开启状态背景色
- protected int mPrimaryDarkColor = 0xFF3AC652; // 开启状态按钮描边色
- protected int mOffColor = 0xFFE3E3E3; // 关闭状态描边色
- protected int mOffDarkColor = 0xFFBFBFBF; // 关闭状态按钮描边色
- protected int mShadowColor = 0xFF333333; // 按钮阴影色
- 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) {
- super(context);
- initialize(null);
- }
- public SwitchButton(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- initialize(attrs);
- }
- public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- initialize(attrs);
- }
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- initialize(attrs);
- }
- private void initialize(AttributeSet attrs) {
- setLayerType(LAYER_TYPE_SOFTWARE, null);
- TypedArray array = getContext().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) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 56, getResources().getDisplayMetrics())
- + getPaddingLeft() + getPaddingRight()), MeasureSpec.EXACTLY);
- break;
- case MeasureSpec.EXACTLY:
- 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:
- break;
- }
- setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
- }
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- isCanVisibleDrawing = w > getPaddingLeft() + getPaddingRight() && h > getPaddingTop() + getPaddingBottom();
- if (isCanVisibleDrawing) {
- int actuallyDrawingAreaWidth = w - getPaddingLeft() - getPaddingRight();
- int actuallyDrawingAreaHeight = h - getPaddingTop() - getPaddingBottom();
- int actuallyDrawingAreaLeft;
- int actuallyDrawingAreaRight;
- int actuallyDrawingAreaTop;
- int actuallyDrawingAreaBottom;
- if (actuallyDrawingAreaWidth * mAspectRatio < actuallyDrawingAreaHeight) {
- actuallyDrawingAreaLeft = getPaddingLeft();
- actuallyDrawingAreaRight = w - 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;
- final float halfHeightOfS = mWidth / 2; // OfB
- mRadius = halfHeightOfS * 0.95f;
- mOffset = mRadius * 0.2f; // offset of switching
- 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;
- mBound.top = top + mStrokeWidth / 2; // bTop = sTop
- mBound.bottom = bottom - mStrokeWidth / 2; // bBottom = sBottom
- 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) {
- result = mOffLeftX; // off -> off2
- } else if (mCheckedState == STATE_SWITCH_ON) {
- result = mOnLeftX - (mOnLeftX - mOn2LeftX) * percent; // on2 -> on
- }
- break;
- case 2:
- if (mCheckedState == STATE_SWITCH_ON) {
- result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent; // off2 -> on
- } else if (mCheckedState == STATE_SWITCH_ON2) {
- result = mOn2LeftX - (mOn2LeftX - mOffLeftX) * percent; // off -> on2
- }
- break;
- case 3:
- result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent; // off -> on
- break;
- case -1:
- if (mCheckedState == STATE_SWITCH_ON2) {
- result = mOn2LeftX + (mOnLeftX - mOn2LeftX) * percent; // on -> on2
- } else if (mCheckedState == STATE_SWITCH_OFF) {
- result = mOffLeftX; // off2 -> off
- }
- break;
- case -2:
- if (mCheckedState == STATE_SWITCH_OFF) {
- result = mOffLeftX + (mOn2LeftX - mOffLeftX) * percent; // on2 -> off
- } else if (mCheckedState == STATE_SWITCH_OFF2) {
- result = mOff2LeftX + (mOnLeftX - mOff2LeftX) * percent; // on -> off2
- }
- break;
- case -3:
- result = mOffLeftX + (mOnLeftX - mOffLeftX) * percent; // on -> off
- break;
- default: // init
- case 0:
- if (mCheckedState == STATE_SWITCH_OFF) {
- result = mOffLeftX; // off -> off
- } else if (mCheckedState == STATE_SWITCH_ON) {
- result = mOnLeftX; // on -> on
- }
- break;
- }
- return result - mOffLeftX;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- if (!isCanVisibleDrawing) 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);
- mPaint.setColor(0xFFFFFFFF);
- 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 (isShadow) {
- 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_DOWN:
- break;
- 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;
- }
- 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) {
- isShadow = 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(OnCheckedChangeListener l) {
- mListener = l;
- }
- public interface OnCheckedChangeListener {
- void onCheckedChanged(SwitchButton button, boolean isChecked);
- }
- /**
- * 保存开关状态
- */
- 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<SavedState> CREATOR = new Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
- }
|