自定义View仿iOS的UiSwitch控件

作者: wo叫天然呆 | 来源:发表于2017-02-11 17:48 被阅读925次

    自定义View仿iOS的UiSwitch控件

    本文原创,转载请注明出处。欢迎关注我的 简书

    前言:

    Android的Switch控件相信大家都用过,其实我觉得效果还好,不过公司要求UI上的统一,所以让我仿iOS效果,我就纳闷了,为什么一直要仿iOS,就不能iOS仿Android么?牢骚发完了,可以开工了。

    附上效果图

    效果如图,看起来还行吧

    思路

    绘制控件

    整个控件在绘制的时候分2个部分:

    1. 底板,也就是那个椭圆形类似跑道的部分。
    2. 按钮。

    这部分没什么需要特别说明的,如果有一点自定义View基础的朋友应该都能轻松搞定。

    动效处理

    动效我这里用的也是最基础的平移动效,同时加上底板颜色渐变效果

    /**
         * 关闭开关
         */
        public void toggleOn() {
            //手柄槽颜色渐变和手柄滑动通过属性动画来实现
            ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", 0, mOffSpotX);
            animator.setDuration(300);
            animator.start();
            animator.setInterpolator(new DecelerateInterpolator());
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float fraction = animation.getAnimatedFraction();
                    calculateColor(fraction, mOffSlotColor, mOpenSlotColor);
                    invalidate();
                }
            });
        }
    
        /**
         * 打开开关
         */
        public void toggleOff() {
            //手柄槽颜色渐变和手柄滑动通过属性动画来实现
            ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", mOffSpotX, 0);
            animator.setDuration(300);
            animator.start();
            animator.setInterpolator(new DecelerateInterpolator());
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float fraction = animation.getAnimatedFraction();
                    calculateColor(fraction, mOpenSlotColor, mOffSlotColor);
                    invalidate();
                }
            });
        }
    
    /**
         * 计算切换时的手柄槽的颜色
         *
         * @param fraction   动画播放进度
         * @param startColor 起始颜色
         * @param endColor   终止颜色
         */
        public void calculateColor(float fraction, int startColor, int endColor) {
            final int fb = Color.blue(startColor);
            final int fr = Color.red(startColor);
            final int fg = Color.green(startColor);
    
            final int tb = Color.blue(endColor);
            final int tr = Color.red(endColor);
            final int tg = Color.green(endColor);
    
            //RGB三通道线性渐变
            int sr = (int) (fr + fraction * (tr - fr));
            int sg = (int) (fg + fraction * (tg - fg));
            int sb = (int) (fb + fraction * (tb - fb));
            //范围限定
            sb = clamp(sb, 0, 255);
            sr = clamp(sr, 0, 255);
            sg = clamp(sg, 0, 255);
    
            mSlotColor = Color.rgb(sr, sg, sb);
        }
    

    Touch事件与Onclick事件

    这里我是自己做了一个手势工具类,专门处理Touch中的手势,没任何难度,只是懒得每个自定义控件里面都写一套重复的代码,所以就做了个工具类,方便今后的开发
    这里也分享给大家(不要吐槽我的类名,我是百度翻译的)

    /**
     * Created by caihan on 2017/2/10.
     * 手势判断工具类,
     */
    public class GestureUtils {
        private static final String TAG = "GestureUtils";
    
        private float startX = 0f;
        private float endX = 0f;
        private float startY = 0f;
        private float endY = 0f;
        private float xDistance = 0f;
        private float yDistance = 0f;
    
        public enum Gesture {
            PullUp, PullDown, PullLeft, PullRight
        }
    
        public GestureUtils() {
    
        }
    
        /**
         * 当event.getAction() == MotionEvent.ACTION_DOWN 的时候调用
         * 设置初始X,Y坐标
         *
         * @param event
         */
        public void actionDown(MotionEvent event) {
            xDistance = yDistance = 0f;
            setStartX(event);
            setStartY(event);
        }
    
        /**
         * 当event.getAction() == MotionEvent.ACTION_MOVE 的时候调用
         * 设置移动的X,Y坐标
         *
         * @param event
         */
        public void actionMove(MotionEvent event) {
            setEndX(event);
            setEndY(event);
        }
    
        /**
         * 当event.getAction() == MotionEvent.ACTION_UP 的时候调用
         * 设置截止的X,Y坐标
         *
         * @param event
         */
        public void actionUp(MotionEvent event) {
            setEndX(event);
            setEndY(event);
        }
    
        /**
         * 手势判断接口
         *
         * @param gesture
         * @return
         */
        public boolean getGesture(Gesture gesture) {
            switch (gesture) {
                case PullUp:
                    return isRealPullUp();
                case PullDown:
                    return isRealPullDown();
                case PullLeft:
                    return isRealPullLeft();
                case PullRight:
                    return isRealPullRight();
                default:
                    LogUtils.e(TAG, "getGesture error");
                    return false;
            }
        }
    
        /**
         * 获取Touch点相对于屏幕原点的X坐标
         *
         * @param event
         * @return
         */
        private float gestureRawX(MotionEvent event) {
            return event.getRawX();
        }
    
        /**
         * 获取Touch点相对于屏幕原点的Y坐标
         *
         * @param event
         * @return
         */
        private float gestureRawY(MotionEvent event) {
            return event.getRawY();
        }
    
        /**
         * 获取X轴偏移量,取绝对值
         *
         * @param startX
         * @param endX
         * @return
         */
        private float gestureDistanceX(float startX, float endX) {
            setxDistance(Math.abs(endX - startX));
            return xDistance;
        }
    
        /**
         * 获取Y轴偏移量,取绝对值
         *
         * @param startY
         * @param endY
         * @return
         */
        private float gestureDistanceY(float startY, float endY) {
            setyDistance(Math.abs(endY - startY));
            return yDistance;
        }
    
        /**
         * endY坐标比startY小,相减负数表示手势上滑
         *
         * @param startY
         * @param endY
         * @return
         */
        private boolean isPullUp(float startY, float endY) {
            return (endY - startY) < 0;
        }
    
        /**
         * endY坐标比startY大,相减正数表示手势下滑
         *
         * @param startY
         * @param endY
         * @return
         */
        private boolean isPullDown(float startY, float endY) {
            return (endY - startY) > 0;
        }
    
        /**
         * endX坐标比startX大,相减正数表示手势右滑
         *
         * @param startX
         * @param endX
         * @return
         */
        private boolean isPullRight(float startX, float endX) {
            return (endX - startX) > 0;
        }
    
        /**
         * endX坐标比startX小,相减负数表示手势左滑
         *
         * @param startX
         * @param endX
         * @return
         */
        private boolean isPullLeft(float startX, float endX) {
            return (endX - startX) < 0;
        }
    
        /**
         * 判断用户真实操作是否是上滑
         *
         * @return
         */
        private boolean isRealPullUp() {
            if (gestureDistanceX(startX, endX) < gestureDistanceY(startY, endY)) {
                //Y轴偏移量大于X轴,表示用户真实目的是上下滑动
                return isPullUp(startY, endY);
            }
            return false;
        }
    
        /**
         * 判断用户真实操作是否是下滑
         *
         * @return
         */
        private boolean isRealPullDown() {
            if (gestureDistanceX(startX, endX) < gestureDistanceY(startY, endY)) {
                //Y轴偏移量大于X轴,表示用户真实目的是上下滑动
                return isPullDown(startY, endY);
            }
            return false;
        }
    
        /**
         * 判断用户真实操作是否是左滑
         *
         * @return
         */
        private boolean isRealPullLeft() {
            if (gestureDistanceX(startX, endX) > gestureDistanceY(startY, endY)) {
                //Y轴偏移量大于X轴,表示用户真实目的是上下滑动
                return isPullLeft(startX, endX);
            }
            return false;
        }
    
        /**
         * 判断用户真实操作是否是左滑
         *
         * @return
         */
        private boolean isRealPullRight() {
            if (gestureDistanceX(startX, endX) > gestureDistanceY(startY, endY)) {
                //Y轴偏移量大于X轴,表示用户真实目的是上下滑动
                return isPullRight(startX, endX);
            }
            return false;
        }
    
    
        private GestureUtils setStartX(MotionEvent event) {
            this.startX = gestureRawX(event);
            return this;
        }
    
        private GestureUtils setEndX(MotionEvent event) {
            this.endX = gestureRawX(event);
            return this;
        }
    
        private GestureUtils setStartY(MotionEvent event) {
            this.startY = gestureRawY(event);
            return this;
        }
    
        private GestureUtils setEndY(MotionEvent event) {
            this.endY = gestureRawY(event);
            return this;
        }
    
        private GestureUtils setxDistance(float xDistance) {
            this.xDistance = xDistance;
            return this;
        }
    
        private GestureUtils setyDistance(float yDistance) {
            this.yDistance = yDistance;
            return this;
        }
    
        public float getStartX() {
            return startX;
        }
    
        public float getEndX() {
            return endX;
        }
    
        public float getStartY() {
            return startY;
        }
    
        public float getEndY() {
            return endY;
        }
    
        public float getxDistance() {
            return xDistance;
        }
    
        public float getyDistance() {
            return yDistance;
        }
    }
    

    大家都知道,Onclick事件是要在Touch的MotionEvent.ACTION_UP事件之后才触发的,也就是说,如果我们dispatchTouchEvent分发Touch事件的时候,当event.getAction() = MotionEvent.ACTION_UP时,返回true,那么Onclick就不会触发,这样的话,我们就能针对不同的事件做不同的处理,我这边就是这样设计的

        private boolean mIsToggleOn = false;//当前开关标记
        private boolean isTouchEvent = false;//是否由滑动事件消费掉
        private boolean isMoveing = false;//是否还在Touch相应中
    
    @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mGestureUtils.actionDown(event);
                    isTouchEvent = false;
                    isMoveing = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    mGestureUtils.actionMove(event);
                    if (mGestureUtils.getGesture(GestureUtils.Gesture.PullLeft)) {
                        //左滑,关闭
                        isTouchEvent = true;
                        touchToggle(false);
                        return true;
                    } else if (mGestureUtils.getGesture(GestureUtils.Gesture.PullRight)) {
                        //右滑,开启
                        isTouchEvent = true;
                        touchToggle(true);
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    break;
                case MotionEvent.ACTION_UP:
                    isMoveing = false;
                    if (isTouchEvent) {
                        //不会触发Onclick事件了
                        return true;
                    }
                    break;
                default:
                    break;
            }
            return super.onTouchEvent(event);
        }
    
        /**
         * Touch事件触发
         * mIsToggleOn是当前状态,当mIsToggleOn != open时做出相应
         *
         * @param open 是否打开
         */
        private void touchToggle(boolean open) {
            if (!isMoveing) {
                isMoveing = true;
                if (mIsToggleOn != open) {
                    if (mIsToggleOn) {
                        toggleOff();
                    } else {
                        toggleOn();
                    }
                    mIsToggleOn = !mIsToggleOn;
                    if (mOnToggleListener != null) {
                        mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
                    }
                }
            }
        }
    
        /**
         * Onclick事件触发
         */
        private void onClickToggle() {
            if (mIsToggleOn) {
                toggleOff();
            } else {
                toggleOn();
            }
            mIsToggleOn = !mIsToggleOn;
            if (mOnToggleListener != null) {
                mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
            }
        }
    
    

    然后监听按钮状态

        public interface OnToggleListener {
            void onSwitchChangeListener(boolean switchState);
        }
    
        public void setOnToggleListener(OnToggleListener listener) {
            mOnToggleListener = listener;
        }
    

    完了完了,就这么简单...什么?要完整代码?好吧

    /**
     * Created by caihan on 2017/2/10.
     * 仿iOS的UiSwitch控件
     */
    public class IosSwitch extends View implements View.OnClickListener {
    
        private static final String TAG = "IosSwitch";
    
        private final int BORDER_WIDTH = 2;//边框宽度
    
        private int mBasePlaneColor = Color.parseColor("#4ebb7f");//底盘颜色,布局描边颜色
        private int mOpenSlotColor = Color.parseColor("#4ebb7f");//开启时手柄滑动槽的颜色
        private int mOffSlotColor = Color.parseColor("#EEEEEE");//关闭时手柄滑动槽的颜色
    
        private int mSlotColor;
    
        private RectF mRect = new RectF();
    
        //绘制参数
        private float mBackPlaneRadius;//底板的圆形半径
        private float mSpotRadius;//手柄半径
    
        private float spotStartX;//手柄的起始X位置,切换时平移改变它
        private float mSpotY;//手柄的起始X位置,不变
        private float mOffSpotX;//关闭时,手柄的水平位置
    
        private Paint mPaint;//画笔
    
        private boolean mIsToggleOn = false;//当前开关标记
        private boolean isTouchEvent = false;//是否由滑动事件消费掉
        private boolean isMoveing = false;//是否还在Touch相应中
    
        private OnToggleListener mOnToggleListener;//toggle事件监听
    
        private GestureUtils mGestureUtils;//手势工具类
    
        public interface OnToggleListener {
            void onSwitchChangeListener(boolean switchState);
        }
    
        public IosSwitch(Context context) {
            super(context);
            init(context);
        }
    
        public IosSwitch(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        private void init(Context context) {
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            setOnClickListener(this);
            setEnabled(true);
            mGestureUtils = new GestureUtils();
        }
    
        @Override
        public void onClick(View v) {
            onClickToggle();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mGestureUtils.actionDown(event);
                    isTouchEvent = false;
                    isMoveing = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    mGestureUtils.actionMove(event);
                    if (mGestureUtils.getGesture(GestureUtils.Gesture.PullLeft)) {
                        //左滑,关闭
                        isTouchEvent = true;
                        touchToggle(false);
                        return true;
                    } else if (mGestureUtils.getGesture(GestureUtils.Gesture.PullRight)) {
                        //右滑,开启
                        isTouchEvent = true;
                        touchToggle(true);
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    break;
                case MotionEvent.ACTION_UP:
                    isMoveing = false;
                    if (isTouchEvent) {
                        //不会触发Onclick事件了
                        return true;
                    }
                    break;
                default:
                    break;
            }
            return super.onTouchEvent(event);
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int wMode = MeasureSpec.getMode(widthMeasureSpec);
            int hMode = MeasureSpec.getMode(heightMeasureSpec);
            int wSize = MeasureSpec.getSize(widthMeasureSpec);
            int hSize = MeasureSpec.getSize(heightMeasureSpec);
            int resultWidth = wSize;
            int resultHeight = hSize;
            Resources r = Resources.getSystem();
            //lp = wrapcontent时 指定默认值
            if (wMode == MeasureSpec.AT_MOST) {
                resultWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
            }
            if (hMode == MeasureSpec.AT_MOST) {
                resultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
            }
            setMeasuredDimension(resultWidth, resultHeight);
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            mBackPlaneRadius = Math.min(getWidth(), getHeight()) * 0.5f;
            mSpotRadius = mBackPlaneRadius - BORDER_WIDTH;
            spotStartX = 0;
            mSpotY = 0;
            mOffSpotX = getMeasuredWidth() - mBackPlaneRadius * 2;
            mSlotColor = mOffSlotColor;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            //画底板
            mRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
            mPaint.setColor(mBasePlaneColor);
            canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
    
            //画手柄的槽
            mRect.set(BORDER_WIDTH,
                    BORDER_WIDTH,
                    getMeasuredWidth() - BORDER_WIDTH,
                    getMeasuredHeight() - BORDER_WIDTH);
    
            mPaint.setColor(mSlotColor);
            canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
    
            //手柄包括包括两部分,深色底板和白板,这样做的目的是使圆盘具有边框
            //手柄的底盘
            mRect.set(spotStartX,
                    mSpotY,
                    spotStartX + mBackPlaneRadius * 2,
                    mSpotY + mBackPlaneRadius * 2);
    
            mPaint.setColor(mBasePlaneColor);
            canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
    
            //手柄的圆板
            mRect.set(spotStartX + BORDER_WIDTH,
                    mSpotY + BORDER_WIDTH,
                    mSpotRadius * 2 + spotStartX + BORDER_WIDTH,
                    mSpotRadius * 2 + mSpotY + BORDER_WIDTH);
    
            mPaint.setColor(Color.WHITE);
            canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
        }
    
        public float getSpotStartX() {
            return spotStartX;
        }
    
        public void setSpotStartX(float spotStartX) {
            this.spotStartX = spotStartX;
        }
    
        /**
         * 计算切换时的手柄槽的颜色
         *
         * @param fraction   动画播放进度
         * @param startColor 起始颜色
         * @param endColor   终止颜色
         */
        public void calculateColor(float fraction, int startColor, int endColor) {
            final int fb = Color.blue(startColor);
            final int fr = Color.red(startColor);
            final int fg = Color.green(startColor);
    
            final int tb = Color.blue(endColor);
            final int tr = Color.red(endColor);
            final int tg = Color.green(endColor);
    
            //RGB三通道线性渐变
            int sr = (int) (fr + fraction * (tr - fr));
            int sg = (int) (fg + fraction * (tg - fg));
            int sb = (int) (fb + fraction * (tb - fb));
            //范围限定
            sb = clamp(sb, 0, 255);
            sr = clamp(sr, 0, 255);
            sg = clamp(sg, 0, 255);
    
            mSlotColor = Color.rgb(sr, sg, sb);
        }
    
        private int clamp(int value, int low, int high) {
            return Math.min(Math.max(value, low), high);
        }
    
        /**
         * 关闭开关
         */
        public void toggleOn() {
            //手柄槽颜色渐变和手柄滑动通过属性动画来实现
            ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", 0, mOffSpotX);
            animator.setDuration(300);
            animator.start();
            animator.setInterpolator(new DecelerateInterpolator());
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float fraction = animation.getAnimatedFraction();
                    calculateColor(fraction, mOffSlotColor, mOpenSlotColor);
                    invalidate();
                }
            });
        }
    
        /**
         * 打开开关
         */
        public void toggleOff() {
            //手柄槽颜色渐变和手柄滑动通过属性动画来实现
            ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", mOffSpotX, 0);
            animator.setDuration(300);
            animator.start();
            animator.setInterpolator(new DecelerateInterpolator());
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float fraction = animation.getAnimatedFraction();
                    calculateColor(fraction, mOpenSlotColor, mOffSlotColor);
                    invalidate();
                }
            });
        }
    
        public boolean getSwitchState() {
            return mIsToggleOn;
        }
    
        /**
         * Touch事件触发
         * mIsToggleOn是当前状态,当mIsToggleOn != open时做出相应
         *
         * @param open 是否打开
         */
        private void touchToggle(boolean open) {
            if (!isMoveing) {
                isMoveing = true;
                if (mIsToggleOn != open) {
                    if (mIsToggleOn) {
                        toggleOff();
                    } else {
                        toggleOn();
                    }
                    mIsToggleOn = !mIsToggleOn;
                    if (mOnToggleListener != null) {
                        mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
                    }
                }
            }
        }
    
        /**
         * Onclick事件触发
         */
        private void onClickToggle() {
            if (mIsToggleOn) {
                toggleOff();
            } else {
                toggleOn();
            }
            mIsToggleOn = !mIsToggleOn;
            if (mOnToggleListener != null) {
                mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
            }
        }
    
        public void setOnToggleListener(OnToggleListener listener) {
            mOnToggleListener = listener;
        }
    
        /**
         * 界面上设置开关初始状态
         * @param open
         */
        public void setChecked(final boolean open) {
            this.postDelayed(new Runnable() {
                @Override
                public void run() {
                    touchToggle(open);
                }
            }, 300);
        }
    }
    

    感谢

    感谢kakacxicm提供的绘制思路与动效处理
    这边再分享几个其他的版本给大家参考:
    SwitchButton
    自定义控件(三步搞定switch)
    Swift-自定义switch控件

    搞定,收工

    欢迎大家留言指出我的不足。

    相关文章

      网友评论

      • 21ac5f35a09e:老哥为什么我的开关是竖直方向的~~~
        wo叫天然呆:@晓_07ba 你长跟宽设置错了吧
      • 小蜜蜂Bee:可以直接把代码放到 github 上吗?
        wo叫天然呆:@小蜜蜂Bee 改天有空的时候放上去,不过这个就2个类而已:smile:

      本文标题:自定义View仿iOS的UiSwitch控件

      本文链接:https://www.haomeiwen.com/subject/vedoittx.html