使用PathMeasure实现 动画CheckBox

先看效果:(最上面那个颜色录像有点问题了)


看见支付宝的支付结果 那个效果还不错,想到那实现一个类似的checkBox吧。

这个view有点击事件,选中监听,快速点击动画流畅,支持wrapcontent,数据持久。

主角是PathMeasure中的getSegment(...):

public boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
Given a start and stop distance, return in dst the intervening segment(s). If the segment is zero-length, return false, else return true. startD and stopD are pinned to legal values (0..getLength()). If startD <= stopD then return false (and leave dst untouched). Begin the segment with a moveTo if startWithMoveTo is true.
On KITKAT and earlier releases, the resulting path may not display on a hardware-accelerated Canvas. A simple workaround is to add a single operation to this path, such as dst.rLineTo(0, 0).

简单理解就是,从原有的path中截取出来其中一段path。

一.使用到的知识点:

1.属性动画,AnimatorSet

2.path,pathMeasure,pathMeasure.getSegment(...);

3.paint ,shader

二.聊聊知识点中注意的地方:

1.属性动画,AnimatorSet:
1).快速点击,防止动画会从0开始,那需要将上次动画结束点作为下次动画的起始点:ObjectAnimator.ofFloat(mPercentCheck, 1);
(这里补充下动画的cancel和end知识点,举个栗子就明白了:
属性动画:value从0到1。
在动画执行一半时,调用anim.cancel():value定格在cancel时(value:0.5),然后调用监听中的onCancel(),然后onEnd()。
在动画执行一半时,调用anim.end():value直接跳到最终值(value:1),然后onEnd().
)
2).要实现圆和对号动画分步进行,AnimatorSet使用顺序播放动画:playSequentially(vaCircleCheck, vaCheckCheck);
2.path,pathMeasure:
1).pathMeasure.getLength()的注释: Return the total length of the current contour。 Current contour !
measureCheck.nextContour()的注释: Move to the next contour in the path。
原来pathMeasure长度还分段的。。意思就是:如果path画了一个圆,然后接着再画一条直线。 那么pathMeasure.getLenght()获取到的长度只为圆的长度。想要得到直线的长度,必须先调用nextContour(),再次getLength()才为直线的长度。测试了好久,这个contour到底怎么区分的还是很晕。。
2). pathMeasure.getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo):从原path中截取出starD至stopD的dst。
startD:起始点
stopD:结束点
dst:截取到的path
startWithMoveTo:true:再次截取,起始点为0时,还是原path的起始点。
false:再次截取,起始点为0时,为上次截取的终点。
3)path.addCircle (float x, float y, float radius, Path.Direction dir):
 要做好padding的处理。需要将padding主动加上圆环的宽度的一半,否则会出界面。
最后一个参数很关键,如果想要让圆换一个方向转,那设置为CW即可(顺时针)。CWW(逆时针)。
3.paint:
1).cap设置为round,线的顶端是圆形。join设置为round,线的拐弯处是圆弧。
4.事件监听:
1).自定义view 中重写了一些监听事件,那么需要再定义一个监听事件,供用户重写。(此view点击事件需要使用OnClickListenerEx)
5.要数据持久处理
1)重写onSaveInstanceState()和onRestoreInstanceState()。
6.动画即时结束
1)onDetachedFromWindow()方法中,将动画cancel()掉。

三.思路

1.画圆环和画对号。
2.通过pathMeasure.getLenght()获取到圆环和对号的路径长度。
3.获取属性动画的值,获取到当前路径长度。
4.用getSegment(...)得到新的path。
5.画出新的path。

四:源码

package com.dup.bitmapdemo.view;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.SweepGradient;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

/**
 * Created by dup on 16-8-2.
 */
public class PathCheckBoxView extends View implements View.OnClickListener {
    private static final String INSTANCE_STATUE = "status";
    private static final String INSTANCE_CHECK = "check";
    private static final String INSTANCE_ENABLE = "enable";

    private Context context;

    private static int duration = 300;
    //wrapcontent时默认大小:dp
    private int defaultSize = 40;
    private int viewSize = defaultSize;

    //背景灰色线条paint
    private Paint mBackPathPaint;
    //前线条paint
    private Paint mDstPaint;

    //背景 圆
    private Path mCirclePath;
    //前景 圆
    private Path mCircleDst;
    //背景 对号
    private Path mCheckPath;
    //前景 对号
    private Path mCheckDst;

    //圆圈 measure
    private PathMeasure measureCircle;
    //对号 measure
    private PathMeasure measureCheck;

    //圆圈长度
    private float lengthCircle;
    //对号长度
    private float lengthCheck;

    //圆圈动画进度
    private float mPercentCircle = 0;
    //对号进度
    private float mPercentCheck = 0;

    //选中动画
    private AnimatorSet asCheck;
    //取消选中动画
    private AnimatorSet asUnCheck;
    //选中动画---圆环动画
    private ValueAnimator vaCheckCheck;
    // ---对号动画
    private ValueAnimator vaCircleCheck;
    private ValueAnimator vaCircleUnCheck;
    private ValueAnimator vaCheckUnCheck;

    private boolean isEnabled = true;

    public boolean isEnabled() {
        return isEnabled;
    }

    public void setEnabled(boolean isEnabled) {
        this.isEnabled = isEnabled;
    }

    /**
     * 标志位
     */
    private boolean isChecked = false;

    public boolean isChecked() {
        return isChecked;
    }

    public void setChecked(boolean is) {
        if (!isEnabled) {
            return;
        }

        isChecked = is;

        if (mCheckChangedListener != null) {
            mCheckChangedListener.onCheckedChanged(this, isChecked);
        }

        if (isChecked) {
            startCheckAnim();
        } else {
            startUnCheckAnim();
        }
    }

    public void toggle() {
        if(isEnabled()){
            isChecked = !isChecked;
            setChecked(isChecked);
        }
    }

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

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

    public PathCheckBoxView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        this.setOnClickListener(this);
        setClickable(true);
    }

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

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            setEnabled(bundle.getBoolean(INSTANCE_ENABLE, true));
            setChecked(bundle.getBoolean(INSTANCE_CHECK, false));
            super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATUE));
        } else {
            super.onRestoreInstanceState(state);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if(asCheck!=null){
            asCheck.cancel();
        }
        if(asUnCheck!=null){
            asUnCheck.cancel();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //当为wrapcontent时,将大小设置为默认大小。
        if(MeasureSpec.getMode(widthMeasureSpec)==MeasureSpec.AT_MOST||MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST){
            float scale = context.getResources().getDisplayMetrics().density;
            int sizePx = (int) (defaultSize * scale + 0.5f);
            setMeasuredDimension(
                    MeasureSpec.getMode(widthMeasureSpec)==MeasureSpec.AT_MOST?sizePx:MeasureSpec.getSize(widthMeasureSpec),
                    MeasureSpec.getMode(heightMeasureSpec)==MeasureSpec.AT_MOST?sizePx:MeasureSpec.getSize(heightMeasureSpec)
                    );
        }else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (changed) {
            initData(left, top, right, bottom);
        }
    }

    /**
     * 初始化具体数据
     *
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    private void initData(int left, int top, int right, int bottom) {

        //初始化画笔宽度
        int paintWidth = right - left - getPaddingLeft() - getPaddingRight();
        int paintHeight = bottom - top - getPaddingTop() - getPaddingBottom();
        int paintStroke = Math.min(paintWidth, paintHeight) / 10;//环宽度设为10分之一

        //重设padding,加上圆环的宽度,因为不设置,圆环一半会在view外
        setPadding(getPaddingLeft() + paintStroke / 2, getPaddingTop() + paintStroke / 2, getPaddingRight() + paintStroke / 2, getPaddingRight() + paintStroke / 2);

        //获取到圆环直径
        viewSize = Math.min(right - left - getPaddingLeft() - getPaddingRight(), bottom - top - getPaddingTop() - getPaddingBottom());

        //初始化背景 线 画笔
        mBackPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackPathPaint.setColor(Color.LTGRAY);
        mBackPathPaint.setStyle(Paint.Style.STROKE);
        mBackPathPaint.setStrokeWidth(paintStroke);
        mBackPathPaint.setStrokeCap(Paint.Cap.ROUND);
        mBackPathPaint.setStrokeJoin(Paint.Join.ROUND);

        //初始化前景 线 画笔
        mDstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDstPaint.setStyle(Paint.Style.STROKE);
        mDstPaint.setStrokeWidth(paintStroke);
        mDstPaint.setColor(Color.BLACK);
        mDstPaint.setStrokeCap(Paint.Cap.ROUND);
        mDstPaint.setStrokeJoin(Paint.Join.ROUND);

        //圆环路径
        mCirclePath = new Path();
        mCircleDst = new Path();

        //对号路径
        mCheckPath = new Path();
        mCheckDst = new Path();

        //pathmeasure
        measureCheck = new PathMeasure();
        measureCircle = new PathMeasure();


        //初始化背景线 路径.这里Direction可以决定动画时圆环的旋转方向
        mCirclePath.addCircle(viewSize / 2 + getPaddingLeft(), viewSize / 2 + getPaddingTop(), viewSize / 2, Path.Direction.CCW);

        //初始化对号背景线 路径(根据百分比画对号)
        float[] floats = new float[2];
        floats[0] = getPaddingLeft() + viewSize / 6;
        floats[1] = viewSize / 2 + getPaddingTop();
        mCheckPath.moveTo(floats[0], floats[1]);
        mCheckPath.lineTo(floats[0] + viewSize / 4, floats[1] + viewSize / 4);
        mCheckPath.lineTo(floats[0] + 4 * viewSize / 6, floats[1] - viewSize / 7);

        //初始化Circle的measure
        measureCircle.setPath(mCirclePath, false);
        lengthCircle = measureCircle.getLength();

        //初始化对号的measure
        measureCheck.setPath(mCheckPath, false);
        measureCheck.nextContour()
        lengthCheck = measureCheck.getLength();

        //设置线条渐变
        mDstPaint.setShader(
                mDstPaint.setShader(new SweepGradient(viewSize / 2 + getPaddingLeft(), viewSize / 2 + getPaddingTop(),
                        new int[]{Color.RED, Color.YELLOW, Color.GREEN, Color.BLUE, Color.RED}
                        , new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f}
                )));
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画背景圆环
        canvas.drawPath(mCirclePath, mBackPathPaint);
        //画背景对号
        canvas.drawPath(mCheckPath, mBackPathPaint);

        mCircleDst.reset();
        mCircleDst.lineTo(0, 0);
        mCheckDst.reset();
        mCheckDst.lineTo(0, 0);

        //主角:根据动画进度和pathmeasure测量出的总长度,获取到当前应有长度,从0开始到应有长度.
        measureCircle.getSegment(0, lengthCircle * mPercentCircle, mCircleDst, true);
        measureCheck.getSegment(0, lengthCheck * mPercentCheck, mCheckDst, true);

        canvas.drawPath(mCheckDst, mDstPaint);
        canvas.drawPath(mCircleDst, mDstPaint);

    }


    /**
     * 开始未选中动画
     */
    private void startUnCheckAnim() {
        //圆圈不选中动画,这里选用mPercentCircle作为动画起始值,是为了快速勾选时动画不闪
        vaCircleUnCheck = ObjectAnimator.ofFloat(mPercentCircle, 0);
        vaCircleUnCheck.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mPercentCircle = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        vaCircleUnCheck.setDuration(duration);

        //对号不选中动画
        vaCheckUnCheck = ObjectAnimator.ofFloat(mPercentCheck, 0);
        vaCheckUnCheck.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mPercentCheck = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        vaCheckUnCheck.setDuration(duration);

        asUnCheck = new AnimatorSet();
        //这里顺序执行anim,保证先执行对号动画。
        asUnCheck.playSequentially(vaCheckUnCheck, vaCircleUnCheck);
        asUnCheck.setInterpolator(new AccelerateDecelerateInterpolator());

        if (asCheck != null && asCheck.isStarted()) {
            asCheck.cancel();
        }
        asUnCheck.start();
    }

    /**
     * 开始选中动画
     */
    private void startCheckAnim() {
        //圆圈选中动画,这里选用mPercentCircle作为动画起始值,是为了快速勾选时动画不闪.
        vaCircleCheck = ObjectAnimator.ofFloat(mPercentCircle, 1);
        vaCircleCheck.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mPercentCircle = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        vaCircleCheck.setDuration(duration);

        //对号选中动画
        vaCheckCheck = ObjectAnimator.ofFloat(mPercentCheck, 1);
        vaCheckCheck.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mPercentCheck = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        vaCheckCheck.setDuration(duration);

        asCheck = new AnimatorSet();
        asCheck.setInterpolator(new AccelerateDecelerateInterpolator());
        asCheck.playSequentially(vaCircleCheck, vaCheckCheck);
        if (asUnCheck != null && asUnCheck.isStarted()) {
            asUnCheck.cancel();
        }
        asCheck.start();
    }

    @Override
    public void onClick(View v) {
        if (mClickListener != null) {
            mClickListener.onClick(v);
        }
        toggle();
    }

    private OnClickListenerEx mClickListener;

    /**
     * 点击事件监听
     *
     * @param mListener
     */
    public void setOnClickListenerEx(OnClickListenerEx mListener) {
        this.mClickListener = mListener;
    }

    public interface OnClickListenerEx {
        void onClick(View v);
    }

    private OnCheckedChangeListener mCheckChangedListener;

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

    public interface OnCheckedChangeListener {
        void onCheckedChanged(View view, boolean isChecked);
    }

}
注意的地方就是使用此view的点击事件需要使用OnClickListenerEx。至于彩虹色,可以修改initData()最后的shader。使用和CheckBox基本没有什么区别(貌似这个view中监听事件不能跟ButterKnife好好相处。。。哈)
主要
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值