实现炫酷的CheckBox,就这么简单

自定义View的第一步:自定义属性。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SmoothCheckBox">
        <!-- 动画持续时间 -->
        <attr name="duration" format="integer"></attr>
        <!-- 边框宽度 -->
        <attr name="strikeWidth" format="dimension|reference"></attr>
        <!-- 边框颜色 -->
        <attr name="borderColor" format="color|reference"></attr>
        <!-- 选中状态的颜色 -->
        <attr name="trimColor" format="color|reference"></attr>
        <!-- 对勾颜色 -->
        <attr name="tickColor" format="color|reference"></attr>
        <!-- 对勾宽度 -->
        <attr name="tickWidth" format="dimension|reference"></attr>
    </declare-styleable>
</resources>

我们把CheckBox取名为SmoothCheckBox(没办法(⊙﹏⊙),这名字挺好听的),定义了几个等等要用到的属性。这一步很简单,相信大家都熟练了。

接下来看一看onMeasure(int widthMeasureSpec, int heightMeasureSpec):

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    if (widthMode == MeasureSpec.EXACTLY) {
        mWidth = widthSize;
    } else {
        mWidth = 40;
    }

    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    if (heightMode == MeasureSpec.EXACTLY) {
        mHeight = heightSize;
    } else {
        mHeight = 40;
    }
    setMeasuredDimension(mWidth, mHeight);
    int size = Math.min(mWidth, mHeight);
    center = size / 2;
    mRadius = (int) ((size - mStrokeWidth) / 2 / 1.2f);
    startPoint.set(center * 14 / 30, center * 28 / 30);
    breakPoint.set(center * 26 / 30, center * 40 / 30);
    endPoint.set(center * 44 / 30, center * 20 / 30);

    downLength = (float) Math.sqrt(Math.pow(startPoint.x - breakPoint.x, 2f) + Math.pow(startPoint.y - breakPoint.y, 2f));
    upLength = (float) Math.sqrt(Math.pow(endPoint.x - breakPoint.x, 2f) + Math.pow(endPoint.y - breakPoint.y, 2f));
    totalLength = downLength + upLength;
}

一开始是测量了SmoothCheckBox的宽、高度,默认的宽高度随便定义了一个,当然你们可以自己去修改和完善它。然后就是设置半径之类的,最后的startPoint、breakPoint、endPoint分别对应着选中时对勾的三个点(至于为何是这几个数字,那完全是经验值);downLength就是startPoint和breakPoint的距离,而相对应的upLength就是breakPoint和endPoint的距离。即以下图示:


Checkbox绘制示意图

在看onDraw(Canvas canvas)之前我们先来看两组动画,分别是选中状态时的动画以及未选中状态的动画:

// 由未选中到选中的动画
private void checkedAnimation() {
    animatedValue = 0f;
    tickValue = 0f;
    // 选中时底色的动画
    mValueAnimator = ValueAnimator.ofFloat(0f, 1.2f, 1f).setDuration(2 * duration / 5);
    mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    // 对勾的动画
    mTickValueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(3 * duration / 5);
    mTickValueAnimator.setInterpolator(new LinearInterpolator());
    mTickValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            // 得到动画执行进度
            tickValue = (float) valueAnimator.getAnimatedValue();
            postInvalidate();
        }
    });
    mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            // 得到动画执行进度
            animatedValue = (float) valueAnimator.getAnimatedValue();
            postInvalidate();
        }
    });
    mValueAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            //当底色的动画完成后再开始对勾的动画
            mTickValueAnimator.start();
            Log.i(TAG," mTickValueAnimator.start();");
        }
    });
    mValueAnimator.start();
}

// 由选中到未选中的动画
private void uncheckedAnimation() {
    animatedValue = 0f;
    mValueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(2 * duration / 5);
    mValueAnimator.setInterpolator(new AccelerateInterpolator());
    mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            animatedValue = (float) valueAnimator.getAnimatedValue();
            postInvalidate();
        }
    });
    mValueAnimator.start();
}

这两组动画在点击SmoothCheckBox的时候会调用。相似的,都是在动画执行中得到动画执行的进度,再来调用postInvalidate();让SmoothCheckBox重绘。看完这个之后就是终极大招onDraw(Canvas canvas)了:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.save();
    drawBorder(canvas);
    drawTrim(canvas);
    if (isChecked) {
        drawTick(canvas);
    }
    canvas.restore();
}

// 画对勾
private void drawTick(Canvas canvas) {
    // 得到画对勾的进度
    float temp = tickValue * totalLength;
    Log.i(TAG, "temp:" + temp + "downlength :" + downLength);
    //判断是否是刚开始画对勾的时候,即等于startPoint
    if (Float.compare(tickValue, 0f) == 0) {
        Log.i(TAG, "startPoint : " + startPoint.x + ", " + startPoint.y);
        path.reset();
        path.moveTo(startPoint.x, startPoint.y);
    }
    // 如果画对勾的进度已经超过breakPoint的时候,即(breakPoint,endPoint]
    if (temp > downLength) {
        path.moveTo(startPoint.x, startPoint.y);
        path.lineTo(breakPoint.x, breakPoint.y);
        Log.i(TAG, "endPoint : " + endPoint.x + ", " + endPoint.y);
        path.lineTo((endPoint.x - breakPoint.x) * (temp - downLength) / upLength + breakPoint.x, (endPoint.y - breakPoint.y) * (temp - downLength) / upLength + breakPoint.y);
    } else {
        //画对勾的进度介于startPoinit和breakPoint之间,即(startPoint,breakPoint]
        Log.i(TAG, "down x : " + (breakPoint.x - startPoint.x) * temp / downLength + ",down y: " + (breakPoint.y - startPoint.y) * temp / downLength);
        path.lineTo((breakPoint.x - startPoint.x) * temp / downLength + startPoint.x, (breakPoint.y - startPoint.y) * temp / downLength + startPoint.y);
    }
    canvas.drawPath(path, tickPaint);
}

// 画边框
private void drawBorder(Canvas canvas) {
    float temp;
    // 通过animatedValue让边框产生一个“OverShooting”的动画
    if (animatedValue > 1f) {
        temp = animatedValue * mRadius;
    } else {
        temp = mRadius;
    }
    canvas.drawCircle(center, center, temp, borderPaint);
}

// 画checkbox内部
private void drawTrim(Canvas canvas) {
    canvas.drawCircle(center, center, (mRadius - mStrokeWidth) * animatedValue, trimPaint);
}

onDraw(Canvas canvas)代码中的逻辑基本都加了注释,主要就是原理搞懂了就比较简单了。在绘制对勾时要区分当前处于绘制对勾的哪种状态,然后对应做处理画出线条,剩下的就简单了。关于SmoothCheckBox的讲解到这里就差不多了。

下面就贴出SmoothCheckBox的完整代码:

public class SmoothCheckBox extends View implements View.OnClickListener {

    // 动画持续时间
    private long duration;
    // 边框宽度
    private float mStrokeWidth;
    // 对勾宽度
    private float mTickWidth;
    // 内饰画笔
    private Paint trimPaint;
    // 边框画笔
    private Paint borderPaint;
    // 对勾画笔
    private Paint tickPaint;
    // 默认边框宽度
    private float defaultStrikeWidth;
    // 默认对勾宽度
    private float defaultTickWidth;
    // 宽度
    private int mWidth;
    // 高度
    private int mHeight;
    // 边框颜色
    private int borderColor;
    // 内饰颜色
    private int trimColor;
    // 对勾颜色
    private int tickColor;
    // 半径
    private int mRadius;
    // 中心点
    private int center;
    // 是否是选中
    private boolean isChecked;
    //对勾向下的长度
    private float downLength;
    //对勾向上的长度
    private float upLength;
    // 对勾的总长度
    private float totalLength;
    // 监听器
    private OnCheckedChangeListener listener;

    private ValueAnimator mValueAnimator;

    private ValueAnimator mTickValueAnimator;

    private float animatedValue;

    private float tickValue;
    // 对勾开始点
    private Point startPoint = new Point();
    // 对勾转折点
    private Point breakPoint = new Point();
    // 对勾结束点
    private Point endPoint = new Point();

    private static final String TAG = "SmoothCheckBox";

    private static final String KEY_INSTANCE_STATE = "InstanceState";

    private Path path = new Path();

    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
        this.listener = listener;
    }

    public SmoothCheckBox(Context context) {
        this(context, null);
    }

    public SmoothCheckBox(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SmoothCheckBox(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox);
        duration = a.getInt(R.styleable.SmoothCheckBox_duration, 600);

        defaultStrikeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics());
        mStrokeWidth = a.getDimension(R.styleable.SmoothCheckBox_strikeWidth, defaultStrikeWidth);
        defaultTickWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics());
        mTickWidth = a.getDimension(R.styleable.SmoothCheckBox_tickWidth, defaultTickWidth);
        borderColor = a.getColor(R.styleable.SmoothCheckBox_borderColor, getResources().getColor(android.R.color.darker_gray));
        trimColor = a.getColor(R.styleable.SmoothCheckBox_trimColor, getResources().getColor(android.R.color.holo_green_light));
        tickColor = a.getColor(R.styleable.SmoothCheckBox_tickColor, getResources().getColor(android.R.color.white));
        a.recycle();

        trimPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        trimPaint.setStyle(Paint.Style.FILL);
        trimPaint.setColor(trimColor);

        borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        borderPaint.setStrokeWidth(mStrokeWidth);
        borderPaint.setColor(borderColor);
        borderPaint.setStyle(Paint.Style.STROKE);

        tickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        tickPaint.setColor(tickColor);
        tickPaint.setStyle(Paint.Style.STROKE);
        tickPaint.setStrokeCap(Paint.Cap.ROUND);
        tickPaint.setStrokeWidth(mTickWidth);

        setOnClickListener(this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode == MeasureSpec.EXACTLY) {
            mWidth = widthSize;
        } else {
            mWidth = 40;
        }

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.EXACTLY) {
            mHeight = heightSize;
        } else {
            mHeight = 40;
        }
        setMeasuredDimension(mWidth, mHeight);
        int size = Math.min(mWidth, mHeight);
        center = size / 2;
        mRadius = (int) ((size - mStrokeWidth) / 2 / 1.2f);
        startPoint.set(center * 14 / 30, center * 28 / 30);
        breakPoint.set(center * 26 / 30, center * 40 / 30);
        endPoint.set(center * 44 / 30, center * 20 / 30);

        downLength = (float) Math.sqrt(Math.pow(startPoint.x - breakPoint.x, 2f) + Math.pow(startPoint.y - breakPoint.y, 2f));
        upLength = (float) Math.sqrt(Math.pow(endPoint.x - breakPoint.x, 2f) + Math.pow(endPoint.y - breakPoint.y, 2f));
        totalLength = downLength + upLength;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        drawBorder(canvas);
        drawTrim(canvas);
        if (isChecked) {
            drawTick(canvas);
        }
        canvas.restore();
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable(KEY_INSTANCE_STATE, super.onSaveInstanceState());
        bundle.putBoolean(KEY_INSTANCE_STATE, isChecked);
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            boolean isChecked = bundle.getBoolean(KEY_INSTANCE_STATE);
            setChecked(isChecked);
            super.onRestoreInstanceState(bundle.getParcelable(KEY_INSTANCE_STATE));
            return;
        }
        super.onRestoreInstanceState(state);
    }

    // 切换状态
    private void toggle() {
        isChecked = !isChecked;
        if (listener != null) {
            listener.onCheckedChanged(this, isChecked);
        }
        if (isChecked) {
            checkedAnimation();
        } else {
            uncheckedAnimation();
        }
    }

    // 由未选中到选中的动画
    private void checkedAnimation() {
        animatedValue = 0f;
        tickValue = 0f;
        mValueAnimator = ValueAnimator.ofFloat(0f, 1.2f, 1f).setDuration(2 * duration / 5);
        mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        mTickValueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(3 * duration / 5);
        mTickValueAnimator.setInterpolator(new LinearInterpolator());
        mTickValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                tickValue = (float) valueAnimator.getAnimatedValue();
                postInvalidate();
            }
        });
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                animatedValue = (float) valueAnimator.getAnimatedValue();
                postInvalidate();
            }
        });
        mValueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mTickValueAnimator.start();
                Log.i(TAG," mTickValueAnimator.start();");
            }
        });
        mValueAnimator.start();
    }

    // 由选中到未选中的动画
    private void uncheckedAnimation() {
        animatedValue = 0f;
        mValueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(2 * duration / 5);
        mValueAnimator.setInterpolator(new AccelerateInterpolator());
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                animatedValue = (float) valueAnimator.getAnimatedValue();
                postInvalidate();
            }
        });
        mValueAnimator.start();
    }

    // 画对勾
    private void drawTick(Canvas canvas) {
        float temp = tickValue * totalLength;
        Log.i(TAG, "temp:" + temp + "downlength :" + downLength);
        if (Float.compare(tickValue, 0f) == 0) {
            Log.i(TAG, "startPoint : " + startPoint.x + ", " + startPoint.y);
            path.reset();
            path.moveTo(startPoint.x, startPoint.y);
        }
        if (temp > downLength) {
            path.moveTo(startPoint.x, startPoint.y);
            path.lineTo(breakPoint.x, breakPoint.y);
            Log.i(TAG, "endPoint : " + endPoint.x + ", " + endPoint.y);
            path.lineTo((endPoint.x - breakPoint.x) * (temp - downLength) / upLength + breakPoint.x, (endPoint.y - breakPoint.y) * (temp - downLength) / upLength + breakPoint.y);
        } else {
            Log.i(TAG, "down x : " + (breakPoint.x - startPoint.x) * temp / downLength + ",down y: " + (breakPoint.y - startPoint.y) * temp / downLength);
            path.lineTo((breakPoint.x - startPoint.x) * temp / downLength + startPoint.x, (breakPoint.y - startPoint.y) * temp / downLength + startPoint.y);
        }
        canvas.drawPath(path, tickPaint);
    }

    // 画边框
    private void drawBorder(Canvas canvas) {
        float temp;
        if (animatedValue > 1f) {
            temp = animatedValue * mRadius;
        } else {
            temp = mRadius;
        }
        canvas.drawCircle(center, center, temp, borderPaint);
    }

    // 画checkbox内部
    private void drawTrim(Canvas canvas) {
        canvas.drawCircle(center, center, (mRadius - mStrokeWidth) * animatedValue, trimPaint);
    }

    @Override
    public void onClick(View view) {
        toggle();
    }

    /**
     * 判断checkbox是否选中状态
     *
     * @return
     */
    public boolean isChecked() {
        return isChecked;
    }

    /**
     * 设置checkbox的状态
     *
     * @param isChecked 是否选中
     */
    public void setChecked(boolean isChecked) {
        this.setChecked(isChecked, false);
    }

    /**
     * 设置checkbox的状态
     *
     * @param isChecked   是否选中
     * @param isAnimation 切换时是否有动画
     */
    public void setChecked(boolean isChecked, boolean isAnimation) {
        this.isChecked = isChecked;
        if (isAnimation) {
            if (isChecked) {
                checkedAnimation();
            } else {
                uncheckedAnimation();
            }
        } else {
            animatedValue = isChecked ? 1f : 0f;
            tickValue = 1f;
            invalidate();
        }
        if (listener != null) {
            listener.onCheckedChanged(this, isChecked);
        }
    }

    public interface OnCheckedChangeListener {
        void onCheckedChanged(SmoothCheckBox smoothCheckBox, boolean isChecked);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值