仿IOS效果的一个简单的ToogleButton

作者: vison123 | 来源:发表于2017-08-27 20:35 被阅读604次

    前言

    项目中总会有涉及到控制开关的东西,比如是否设置默认的配置,是否开启夜光模式,是否浏览中加载图片....界面都是需要一个显示的开关图标的,最简单的方法是使用checkButton,用两张图片作为drawable的资源使用,这是很容易做到的。点击的时候就是两张图片的切换,但是这样做有很明显的缺点

    • 动画很突兀,是一瞬间完成的,缺少观赏性
    • 第二就麻烦了,当你找不到你所需要的图片的时候,此不是欢声笑语中打出GG?
    GG

    所以我们有必要自己写一个view来代替这种东西,苹果手机自带的ToogleButton就是一个很好的选择,他们自带这种的确实比较漂亮。所以今天去撸一个仿ios的ToogleButton。

    正文

    先不说辣么多,老板,上效果图先



    看上去还挺简单的,但里边可不是可不是两张图片换来换去,是自己通过自定义view画上去的,哈哈。里边还有一些动画细节你可能没看到。,我将动画时间设置得长一点你就看得清楚了。来,看下慢动作。



    这样一来就看清楚里边的动画了吧,看清楚了就简单了

    流程分析

    自定义属性

      <declare-styleable name="ToogleButton">
            <attr name="border_width" format="dimension"></attr>
            <attr name="border_color" format="color"></attr>
            <attr name="bg_color" format="color"></attr>
            <attr name="checked_color" format="color"></attr>
            <attr name="circle_radius" format="dimension"></attr>
            <attr name="shadow_color" format="color"></attr>
            <attr name="button_color" format="color"></attr>
            <attr name="animation_duration" format="integer"></attr>
        </declare-styleable>
    

    见名知义,相信大家都看得懂。
    接下来来分析一下draw方法的具体步骤:【可以看着动画来分析】

    • 首先画一个一个白色背景,然后画一个边界线
    • 画环形宽度渐变的环形圆角矩形,它是怎么样渐变的呢?是根据属性动画完成的,这里有个很巧妙的方法实现这个,将画笔的style设置成Paint.Style.STROKE,然后将画笔宽度设置成环形宽度,就可以很容易实现这个效果,至于渐变,当从关闭状态到开启状态的时候,它就是从0到
      圆形按钮半径按钮的一半渐变的,反之,当从开启状态到关闭状态的时候,它就是从圆形按钮半径按钮的一半到0渐变的,大家看看动画就知道了。比例值是根据圆形按钮的滑动的完成度计算的。
    • 画圆形按钮 根据属性动画的比例算出mButtonX的值,然后根据这个值确定按钮的位置。下面的红色就是按钮需要滑动的轨迹,你可以根据属性动画的比例值算出当前时间占有这条线的宽度。
    image.png

    还有一个主意的地方,这个背景色是一个渐变色来的,是从白色到绿色的渐变过程。这个使用了
    ArgbEvaluator,这个这个类渐变色专用的,挺方便的。

    private final android.animation.ArgbEvaluator argbEvaluator
                = new android.animation.ArgbEvaluator();
    
    bgColor = (int) argbEvaluator.evaluate(
                            (Float) animation.getAnimatedValue(),
                            getResources().getColor(R.color.white),
                            checkedColor
                    );
    
    • 画完按钮会有这个问题,看下面的图
    image.png

    可以看到圆形按钮左边有个空白的位置,这个不太好看,我们需要用背景色把这段空白遮住。
    就是画一个半圆和一个矩形把它遮住

    paint.setStyle(Paint.Style.FILL);
            paint.setStrokeWidth(1);
            canvas.drawArc(new RectF(left, top,
                            left + 2 * viewRadius, top + 2 * viewRadius),
                    90, 180, true, paint);
            canvas.drawRect(
                    left + viewRadius, top,
                    mButtonX, top + 2 * viewRadius,
                    paint);
    

    完整的onDraw代码如下

    @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            paint.setStrokeWidth(borderWidth);
            paint.setColor(getResources().getColor(R.color.white));
            paint.setStyle(Paint.Style.FILL);
            //绘制白色背景
            drawWhitBg(canvas, paint);
            //绘制边线
            paint.setStyle(Paint.Style.STROKE);
            paint.setColor(borderColor);
            drawWhitBg(canvas, paint);
            //绘制按钮滑动过程中的渐变边框
            float des = (mButtonX * viewRadius / (width - 2 * viewRadius)) * 0.5f;//滑动过程中渐变背景框的半径
            paint.setColor(bgColor);
            paint.setStrokeWidth(des * 2);
            paint.setStyle(Paint.Style.STROKE);
            canvas.drawRoundRect(new RectF(left + des, top + des, right - des, bottom - des), viewRadius, viewRadius, paint);
    
            //填充按钮左边因为画渐变边框而留下来的白框
            paint.setStyle(Paint.Style.FILL);
            paint.setStrokeWidth(1);
            canvas.drawArc(new RectF(left, top,
                            left + 2 * viewRadius, top + 2 * viewRadius),
                    90, 180, true, paint);
            canvas.drawRect(
                    left + viewRadius, top,
                    mButtonX, top + 2 * viewRadius,
                    paint);
            //绘制按钮
            paint.setColor(buttonColor);
            paint.setStyle(Paint.Style.FILL);
            paint.setStrokeWidth(2);
            canvas.drawCircle(buttonRadius + mButtonX, centerY, buttonRadius, paint);
            paint.setColor(shadowColor);
            paint.setStyle(Paint.Style.STROKE);
            canvas.drawCircle(buttonRadius + mButtonX, centerY, buttonRadius, paint);
        }
    

    分析完onDraw方法的流程,其他的东西就比较容易了,这里我没有在onTouchEvent处理Move事件了,感觉没必要。如果你要处理也是可以这基础上加,在move事件的时候加些逻辑。onTouchEvent代码如下

    @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!isEnabled()) {
                return false;
            }
    
            int eventAsked = event.getActionMasked();
            switch (eventAsked) {
                case MotionEvent.ACTION_DOWN:
                    if (isAnimation) {//正在动画中,不处理事件。等待完成在处理
                        return false;
                    }
                    if (checkState == UNCHECKED) {//检测当前状态
                        toogleOn();
                    } else if (checkState == CHECKED) {
                        toogleOff();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            return true;
        }
    
    /**
         * 打开
         */
        private void toogleOn() {
            isAnimation = true;
            valueAnimator.start();
        }
    
        /**
         * 关闭
         */
        private void toogleOff() {
            isAnimation = true;
            valueAnimator.start();
        }
    

    onTouchEvent的逻辑比较简单,这里就不分析了。主要处理Down事件。如果当前是在动画中。也就是按钮在滑动,就不处理这个点击事件。

    结语

    完成的逻辑看下代码就知道了。实现还是比较简单的。记录一下,滴~打卡

    完整代码如下

    public class ToogleButton extends View {
    
        private static final int DEFAULT_TOOGLE_WIDTH = 58;//默认的宽度
        private static final int DEFAULT_TOOGLE_HEIGHT = 36;//默认的高度
    
        private int borderWidth;//边线宽度
    
        private int borderColor;//边线颜色
    
        private int bgColor;//背景颜色
    
        private int checkedColor;//开关为开的时候的背景色
    
        private int circleRadius;//按钮的半径
    
        private int shadowColor;//开关切换时需要绘制的一层背景色
    
        private int buttonColor;//按钮的颜色
    
        private int animationDuration;//动画时间
    
        private Paint paint;
    
        private int checkState = 1;//按钮的开关状态【默认为关闭状态】
    
        private static final int CHECKED = 0;//打开状态
    
        private static final int UNCHECKED = 1;//关闭状态
    
        private boolean isAnimation = false;//是否在滑动中
    
        /**
         * 背景位置
         */
        private float left;
        private float top;
        private float right;
        private float bottom;
        private float centerX;
        private float centerY;
    
        private float height;//背景高度
    
        private float width;//背景宽度
    
        private float viewRadius;//背景半径
    
        private float buttonRadius;//按钮半径
    
        private float mButtonX;//按钮的偏移量
    
        private ValueAnimator valueAnimator;
    
        private final android.animation.ArgbEvaluator argbEvaluator
                = new android.animation.ArgbEvaluator();
    
        public ToogleButton(Context context) {
            this(context, null);
        }
    
        public ToogleButton(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public ToogleButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ToogleButton, defStyleAttr, 0);
            borderWidth = (int) array.getDimension(R.styleable.ToogleButton_border_width,
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
            borderColor = array.getColor(R.styleable.ToogleButton_border_color, getResources().getColor(R.color.toogle_gray));
            bgColor = array.getColor(R.styleable.ToogleButton_bg_color, getResources().getColor(R.color.white));
            checkedColor = array.getColor(R.styleable.ToogleButton_checked_color, getResources().getColor(R.color.toogle_green));
            circleRadius = array.getDimensionPixelOffset(R.styleable.ToogleButton_circle_radius, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20,
                    getResources().getDisplayMetrics()));
            shadowColor = array.getColor(R.styleable.ToogleButton_shadow_color, getResources().getColor(R.color.toogle_gray));
            buttonColor = array.getColor(R.styleable.ToogleButton_button_color, getResources().getColor(R.color.white));
            animationDuration = array.getInt(R.styleable.ToogleButton_animation_duration,500);
            array.recycle();
            init();
        }
    
        /**
         * 初始化一些变量设置
         */
        private void init() {
            paint = new Paint();
            paint.setAntiAlias(true);
            paint.setDither(true);
            paint.setStyle(Paint.Style.STROKE);
    
            valueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(animationDuration);
            valueAnimator.setRepeatCount(0);
            valueAnimator.addUpdateListener(animatorUpdateListener);
            valueAnimator.addListener(animatorListener);
        }
    
        private ValueAnimator.AnimatorListener animatorListener = new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
    
            }
    
            @Override
            public void onAnimationEnd(Animator animation) {
                if (checkState == UNCHECKED) {
                    checkState = CHECKED;
                    isAnimation = false;
                    if (null != onCheckListener) {
                        onCheckListener.onCheck(true);
                    }
                } else if (checkState == CHECKED) {
                    checkState = UNCHECKED;
                    isAnimation = false;
                    if (null != onCheckListener) {
                        onCheckListener.onCheck(false);
                    }
                }
            }
    
            @Override
            public void onAnimationCancel(Animator animation) {
    
            }
    
            @Override
            public void onAnimationRepeat(Animator animation) {
    
            }
        };
    
        private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float totalOffset = width - 2 * buttonRadius;
                if (checkState == UNCHECKED) {//关闭状态时
                    mButtonX = totalOffset * (Float) animation.getAnimatedValue();
                    bgColor = (int) argbEvaluator.evaluate(
                            (Float) animation.getAnimatedValue(),
                            getResources().getColor(R.color.white),
                            checkedColor
                    );
                } else if (checkState == CHECKED) {//打开状态时
                    mButtonX = totalOffset - totalOffset * (Float) animation.getAnimatedValue();
                    bgColor = (int) argbEvaluator.evaluate(
                            (Float) animation.getAnimatedValue(),
                            checkedColor, getResources().getColor(R.color.white)
                    );
                }
                postInvalidate();
            }
        };
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthSpec = MeasureSpec.getMode(widthMeasureSpec);
            int heightSpec = MeasureSpec.getMode(heightMeasureSpec);
    
            if (widthSpec == MeasureSpec.AT_MOST) {
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_TOOGLE_WIDTH, MeasureSpec.EXACTLY);
            }
    
            if (heightSpec == MeasureSpec.AT_MOST) {
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_TOOGLE_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);
    
            height = h - borderWidth - borderWidth;
            width = w - borderWidth - borderWidth;
    
            viewRadius = height * 0.5f;
            buttonRadius = viewRadius - borderWidth;
    
            left = borderWidth;
            top = borderWidth;
            right = w - borderWidth;
            bottom = h - borderWidth;
    
            centerX = (left + right) * 0.5f;
            centerY = (top + bottom) * 0.5f;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            paint.setStrokeWidth(borderWidth);
            paint.setColor(getResources().getColor(R.color.white));
            paint.setStyle(Paint.Style.FILL);
            //绘制白色背景
            drawWhitBg(canvas, paint);
            //绘制边线
            paint.setStyle(Paint.Style.STROKE);
            paint.setColor(borderColor);
            drawWhitBg(canvas, paint);
            //绘制按钮滑动过程中的渐变边框
            float des = (mButtonX * viewRadius / (width - 2 * viewRadius)) * 0.5f;//滑动过程中渐变背景框的半径
            paint.setColor(bgColor);
            paint.setStrokeWidth(des * 2);
            paint.setStyle(Paint.Style.STROKE);
            canvas.drawRoundRect(new RectF(left + des, top + des, right - des, bottom - des), viewRadius, viewRadius, paint);
    
            //填充按钮左边因为画渐变边框而留下来的白框
            paint.setStyle(Paint.Style.FILL);
            paint.setStrokeWidth(1);
            canvas.drawArc(new RectF(left, top,
                            left + 2 * viewRadius, top + 2 * viewRadius),
                    90, 180, true, paint);
            canvas.drawRect(
                    left + viewRadius, top,
                    mButtonX, top + 2 * viewRadius,
                    paint);
            //绘制按钮
            paint.setColor(buttonColor);
            paint.setStyle(Paint.Style.FILL);
            paint.setStrokeWidth(2);
            canvas.drawCircle(buttonRadius + mButtonX, centerY, buttonRadius, paint);
            paint.setColor(shadowColor);
            paint.setStyle(Paint.Style.STROKE);
            canvas.drawCircle(buttonRadius + mButtonX, centerY, buttonRadius, paint);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!isEnabled()) {
                return false;
            }
    
            int eventAsked = event.getActionMasked();
            switch (eventAsked) {
                case MotionEvent.ACTION_DOWN:
                    if (isAnimation) {
                        return false;
                    }
                    if (checkState == UNCHECKED) {
                        toogleOn();
                    } else if (checkState == CHECKED) {
                        toogleOff();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            return true;
        }
    
        /**
         * 打开
         */
        private void toogleOn() {
            isAnimation = true;
            valueAnimator.start();
        }
    
        /**
         * 关闭
         */
        private void toogleOff() {
            isAnimation = true;
            valueAnimator.start();
        }
    
        /**
         * 绘制背景
         *
         * @param canvas
         * @param paint
         */
        private void drawWhitBg(Canvas canvas, Paint paint) {
            canvas.drawRoundRect(new RectF(left, top, right, bottom), viewRadius, viewRadius, paint);
        }
    
        /**
         * 定义一个选中接口回调
         */
        OnCheckListener onCheckListener;
        public interface OnCheckListener{
            void onCheck(boolean isCheck);
        }
        public void setOnCheckListener(OnCheckListener onCheckListener){
            this.onCheckListener = onCheckListener;
        }
    }
    

    相关文章

      网友评论

      • 缘狄:发现一个小bug,不知道你发现没有,如果view在可滑动的viewgroup里时,会直接被选中,这个会误操作,我在onTouchEvent里做了一些判断,把这个完善了

      本文标题:仿IOS效果的一个简单的ToogleButton

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