Android 自定义viewAndroid开发经验谈Android开发

仿IOS效果的一个简单的ToogleButton

2017-08-27  本文已影响604人  vison123

前言

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

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方法的具体步骤:【可以看着动画来分析】

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;
    }
}
上一篇下一篇

猜你喜欢

热点阅读