Android自定义View(仿打钩签到动画)

首先让我们来看一下需要实现的效果
在这里插入图片描述

不想学习想要娱乐的时候忽然看到了这个动画效果,出于程序员的本能,就仿出来了。
闲话不多说,开整。

实现原理

我们看实现的动画效果,其实是分为

1. 绘制未选中状态图形(圆弧和对号)
2. 绘制选中状态圆弧的旋转的动画
3. 绘制选中状态圆弧向中心收缩铺满动画
4. 绘制选中状态对号
5. 绘制选中状态下圆的放大回弹动画
6. 暴露接口接口回调传递选中未选中状态

我们一步一步来实现
首先我们完成准备工作
自定义属性attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomTickView">
        <!--选中情况下基本颜色-->
        <attr name="check_base_color" format="color" />
        <!--选中情况下对号颜色-->
        <attr name="check_tick_color" format="color" />
        <!--未选中情况下基本颜色-->
        <attr name="uncheck_base_color" format="color" />
        <!--未选中情况下对号颜色-->
        <attr name="uncheck_tick_color" format="color" />
        <!--自定义动画执行时间-->
        <attr name="custom_duration" format="integer" />
        <!--控件大小-->
        <attr name="custom_size" format="dimension" />
    </declare-styleable>
</resources>

获取自定义属性并初始化画笔

    private int mCustomSize;//画布大小
    private int mRadius;
    private int mCheckBaseColor;//选中状态基本颜色
    private int mCheckTickColor;//选中状态对号颜色
    private int mUnCheckTickColor;//未选中状态对号颜色
    private int mUnCheckBaseColor;//未选中状态基本颜色
    private Paint mCheckPaint;//选中状态画笔 下面的背景圆
    private Paint mCheckArcPaint;//选中状态画笔 下面的背景圆圆弧
    private Paint mCheckDeclinePaint;//选中状态画笔  (上面的随动画缩减的圆盖在上面)  和对号
    private Paint mUnCheckPaint;//未选中状态画笔
    private Paint mCheckTickPaint;//选中对号画笔
    private Paint mCheckPaintArc;//回弹圆画笔 设置不同宽度已达到回弹圆动画目的
    private boolean isCheckd = false;//选中状态
    private float[] mPoints;
    private int mCenter;
/**
     * 获取自定义属性
     *
     * @param context
     * @param attrs
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomTickView);
        mCustomSize = (int) typedArray.getDimension(R.styleable.CustomTickView_custom_size, dip2px(130));
        mCheckBaseColor = typedArray.getColor(R.styleable.CustomTickView_check_base_color, mCheckBaseColor);
        mCheckTickColor = typedArray.getColor(R.styleable.CustomTickView_check_tick_color, mCheckTickColor);
        mUnCheckBaseColor = typedArray.getColor(R.styleable.CustomTickView_uncheck_base_color, mUnCheckBaseColor);
        mUnCheckTickColor = typedArray.getColor(R.styleable.CustomTickView_uncheck_tick_color, mUnCheckTickColor);
        typedArray.recycle();
        mCenter = mCustomSize / 2;
        mRadius = mCenter - 50;//缩小圆半径大小 防止回弹动画弹出画布
    }
/***
     * 初始化画笔
     */
    private void initPaint() {
        mCheckPaint = new Paint();
        mCheckPaint.setAntiAlias(true);
        mCheckPaint.setColor(mCheckBaseColor);

        mCheckPaintArc = new Paint();
        mCheckPaintArc.setAntiAlias(true);
        mCheckPaintArc.setColor(mCheckBaseColor);


        mCheckArcPaint = new Paint();
        mCheckArcPaint.setAntiAlias(true);
        mCheckArcPaint.setColor(mCheckBaseColor);
        mCheckArcPaint.setStyle(Paint.Style.STROKE);
        mCheckArcPaint.setStrokeWidth(20);


        mCheckDeclinePaint = new Paint();
        mCheckDeclinePaint.setAntiAlias(true);
        mCheckDeclinePaint.setColor(Color.parseColor("#3E3E3E"));

        mUnCheckPaint = new Paint();
        mUnCheckPaint.setAntiAlias(true);
        mUnCheckPaint.setColor(mUnCheckBaseColor);
        mUnCheckPaint.setStyle(Paint.Style.STROKE);
        mUnCheckPaint.setStrokeWidth(20);

        mCheckTickPaint = new Paint();
        mCheckTickPaint.setAntiAlias(true);
        mCheckTickPaint.setColor(mCheckTickColor);
        mCheckTickPaint.setStyle(Paint.Style.STROKE);
        mCheckTickPaint.setStrokeWidth(20);

    }

测量布局

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(mCustomSize, mCustomSize);//正方形布局长宽一致
    }

准备工作完成,接下来我们按照刚才的步骤一步一步实现

1. 绘制未选中状态图形(圆弧和对号)

  @Override
    protected void onDraw(Canvas canvas) {
        if (mCustomSize > 0) {
            if (!isCheckd) {
                canvas.drawCircle(mCenter, mCenter, mRadius, mUnCheckPaint);//未选中状态的圆
                canvas.drawLines(mPoints, mUnCheckPaint);
                return;
            }
        }    
    }

在这里插入图片描述

2. 绘制选中状态圆弧的旋转的动画

 @Override
    protected void onDraw(Canvas canvas) {
    //省略以上代码
     mRingCounter += 10;//按照如下速率转动
            if (mRingCounter >= 360) {
                mRingCounter = 360;
            }
            canvas.drawArc(mRectF, 90, mRingCounter, false, mCheckArcPaint);
}

在这里插入图片描述

3. 绘制选中状态圆弧向中心收缩铺满动画

这里向中心收缩的动画我们可以逆向思维
先绘制指定颜色的整体圆,然后在指定颜色整体圆的图层上在绘制一个背景色的圆(半径不断缩小) 半径不断缩小,背景就不断露出来,达到向中心收缩的效果。
@Override
protected void onDraw(Canvas canvas) {
//省略以上代码
if (mRingCounter == 360) {
//先绘制指定颜色的圆
canvas.drawCircle(mCenter, mCenter, mRadius, mCheckPaint);
//然后在指定颜色的图层上,再绘制背景色的圆(半径不断缩小) 半径不断缩小,背景就不断露出来,达到向中心收缩的效果
mCircleCounter += 10;
canvas.drawCircle(mCenter, mCenter, mRadius - mCircleCounter, mCheckDeclinePaint);
}

在这里插入图片描述

4. 绘制选中状态对号

我们让对号出现的有延迟效果并加上透明出现的效果

 @Override
        protected void onDraw(Canvas canvas) {
        //省略以上代码
          if (mCircleCounter >= mRadius + 100) {//做延迟效果
                    mAlphaCount += 20;
                    if (mAlphaCount >= 255) mAlphaCount = 255; //显示对号(外加一个透明的渐变)
                    mCheckTickPaint.setAlpha(mAlphaCount);//设置透明度
                    //画白色的对号
                    canvas.drawLines(mPoints, mCheckTickPaint);
                  }
    }

在这里插入图片描述

5. 绘制选中状态下圆的放大回弹动画

这里我们的实现思路是在整体圆图层上在画一个圆弧,圆弧的宽度由增大到缩小最后直至为0,这样达到放大回弹的效果

@Override
    protected void onDraw(Canvas canvas) {
        if (mCustomSize > 0) {
            if (!isCheckd) {
                canvas.drawCircle(mCenter, mCenter, mRadius, mUnCheckPaint);//未选中状态的圆
                canvas.drawLines(mPoints, mUnCheckPaint);
                return;
            }
            mRingCounter += 10;
            if (mRingCounter >= 360) {
                mRingCounter = 360;
            }
            canvas.drawArc(mRectF, 90, mRingCounter, false, mCheckArcPaint);
            if (mRingCounter == 360) {
                //先绘制指定颜色的圆
                canvas.drawCircle(mCenter, mCenter, mRadius, mCheckPaint);
                //然后在指定颜色的图层上,再绘制背景色的圆(半径不断缩小) 半径不断缩小,背景就不断露出来,达到向中心收缩的效果
                mCircleCounter += 10;
                canvas.drawCircle(mCenter, mCenter, mRadius - mCircleCounter, mCheckDeclinePaint);
                if (mCircleCounter >= mRadius + 100) {
                    mAlphaCount += 20;
                    if (mAlphaCount >= 255) mAlphaCount = 255; //显示对号(外加一个透明的渐变)
                    mCheckTickPaint.setAlpha(mAlphaCount);//设置透明度
                    //画白色的对号
                    canvas.drawLines(mPoints, mCheckTickPaint);
                    scaleCounter -= 4;//获取是否回弹
                    if (scaleCounter <= -50) {//scaleCounter从大于0到小于0的过程中 画笔宽度也是由增加到减少最后减为0 实现了圆放大收缩的回弹效果
                        scaleCounter = -50;
                    }
                    //放大并回弹,设置画笔的宽度
                    float strokeWith = mCheckArcPaint.getStrokeWidth() +
                            (scaleCounter > 0 ? 6 : -6);
                    System.out.println(strokeWith);
                    mCheckArcPaint.setStrokeWidth(strokeWith);
                    canvas.drawArc(mRectArc, 90, 360, false, mCheckArcPaint);
                }

            }
            postInvalidate();//重绘
        }

    }

以上我们就实现了所有的动画效果
下面我们需要定义接口并暴露接口

6. 暴露接口接口回调传递选中未选中状态

/**
     * 初始化点击事件
     */
    public void setUpEvent() {
        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                isCheckd = !isCheckd;
                reset();
                if (mOnCheckedChangeListener != null) {
                    //此处回调
                    mOnCheckedChangeListener.onCheckedChanged((CustomTickView) view, isCheckd);
                }
            }
        });
    }

    private OnCheckedChangeListener mOnCheckedChangeListener;

    public interface OnCheckedChangeListener {
        void onCheckedChanged(CustomTickView tickView, boolean isCheckd);
    }

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

现在我们就可以通过点击自定义控件实现动画并且接受选中状态

 customTickView.setUpEvent();//运行动画
        customTickView.setOnCheckedChangeListener(new CustomTickView.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CustomTickView tickView, boolean isCheckd) {
                if(!isCheckd){
                    tv_show.setText("未完成签到");
                }else{
                    tv_show.setText("已签到");
                }

            }
        });

附上所有代码:
CustomTickView.java

/**
 * @description:TODO
 * @Author MRyan
 * @Date 2020/2/29 16:17
 * @Version 1.0
 */
public class CustomTickView extends View {
    private int mCustomSize;//画布大小
    private int mRadius;
    private int mCheckBaseColor;//选中状态基本颜色
    private int mCheckTickColor;//选中状态对号颜色
    private int mUnCheckTickColor;//未选中状态对号颜色
    private int mUnCheckBaseColor;//未选中状态基本颜色
    private Paint mCheckPaint;//选中状态画笔 下面的背景圆
    private Paint mCheckArcPaint;//选中状态画笔 下面的背景圆圆弧
    private Paint mCheckDeclinePaint;//选中状态画笔  (上面的随动画缩减的圆盖在上面)  和对号
    private Paint mUnCheckPaint;//未选中状态画笔
    private Paint mCheckTickPaint;//选中对号画笔
    private Paint mCheckPaintArc;//回弹圆画笔 设置不同宽度已达到回弹圆动画目的
    private boolean isCheckd = false;//选中状态
    private float[] mPoints;
    private int mCenter;
    private RectF mRectF;
    private int mRingCounter;
    private int mCircleCounter = 0;//盖在上面的背景色圆逐渐缩小  逆向思维模拟向圆心收缩动画
    private int mAlphaCount = 0;
    private int scaleCounter = 50;
    private RectF mRectArc;

    public CustomTickView(Context context) {
        super(context);
    }

    public CustomTickView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttrs(context, attrs);//获取自定义属性
        initPaint();//初始化画笔
    }


    public CustomTickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, 0);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(mCustomSize, mCustomSize);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mCustomSize > 0) {
            if (!isCheckd) {
                canvas.drawCircle(mCenter, mCenter, mRadius, mUnCheckPaint);//未选中状态的圆
                canvas.drawLines(mPoints, mUnCheckPaint);
                return;
            }
            mRingCounter += 10;
            if (mRingCounter >= 360) {
                mRingCounter = 360;
            }
            canvas.drawArc(mRectF, 90, mRingCounter, false, mCheckArcPaint);
            if (mRingCounter == 360) {
                //先绘制指定颜色的圆
                canvas.drawCircle(mCenter, mCenter, mRadius, mCheckPaint);
                //然后在指定颜色的图层上,再绘制背景色的圆(半径不断缩小) 半径不断缩小,背景就不断露出来,达到向中心收缩的效果
                mCircleCounter += 10;
                canvas.drawCircle(mCenter, mCenter, mRadius - mCircleCounter, mCheckDeclinePaint);
                if (mCircleCounter >= mRadius + 100) {
                    mAlphaCount += 20;
                    if (mAlphaCount >= 255) mAlphaCount = 255; //显示对号(外加一个透明的渐变)
                    mCheckTickPaint.setAlpha(mAlphaCount);//设置透明度
                    //画白色的对号
                    canvas.drawLines(mPoints, mCheckTickPaint);
                    scaleCounter -= 4;//获取是否回弹
                    if (scaleCounter <= -50) {//scaleCounter从大于0到小于0的过程中 画笔宽度也是由增加到减少最后减为0 实现了圆放大收缩的回弹效果
                        scaleCounter = -50;
                    }
                    //放大并回弹,设置画笔的宽度
                    float strokeWith = mCheckArcPaint.getStrokeWidth() +
                            (scaleCounter > 0 ? 6 : -6);
                    System.out.println(strokeWith);
                    mCheckArcPaint.setStrokeWidth(strokeWith);
                    canvas.drawArc(mRectArc, 90, 360, false, mCheckArcPaint);
                }

            }
            postInvalidate();//重绘
        }

    }

    /**
     * 获取自定义属性
     *
     * @param context
     * @param attrs
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomTickView);
        mCustomSize = (int) typedArray.getDimension(R.styleable.CustomTickView_custom_size, dip2px(130));
        mCheckBaseColor = typedArray.getColor(R.styleable.CustomTickView_check_base_color, mCheckBaseColor);
        mCheckTickColor = typedArray.getColor(R.styleable.CustomTickView_check_tick_color, mCheckTickColor);
        mUnCheckBaseColor = typedArray.getColor(R.styleable.CustomTickView_uncheck_base_color, mUnCheckBaseColor);
        mUnCheckTickColor = typedArray.getColor(R.styleable.CustomTickView_uncheck_tick_color, mUnCheckTickColor);
        typedArray.recycle();
        mCenter = mCustomSize / 2;
        mRadius = mCenter - 50;//缩小圆半径大小 防止回弹动画弹出画布
        mPoints = new float[8];
        //简易模拟对号 未做适配
        mPoints[0] = mCenter - mCenter / 3;
        mPoints[1] = mCenter;
        mPoints[2] = mCenter;
        mPoints[3] = mCenter + mCenter / 4;
        mPoints[4] = mCenter - 8;
        mPoints[5] = mCenter + mCenter / 4;
        mPoints[6] = mCenter + mCenter / 2;
        mPoints[7] = mCenter - mCenter / 5;

        mRectF = new RectF(mCenter - mRadius, mCenter - mRadius, mCenter + mRadius, mCenter + mRadius);//选中状态的圆弧 动画
        mRectArc = new RectF(mCenter - mRadius, mCenter - mRadius, mCenter + mRadius, mCenter + mRadius);//选中状态的圆弧 动画

    }

    /***
     * 初始化画笔
     */
    private void initPaint() {
        mCheckPaint = new Paint();
        mCheckPaint.setAntiAlias(true);
        mCheckPaint.setColor(mCheckBaseColor);

        mCheckPaintArc = new Paint();
        mCheckPaintArc.setAntiAlias(true);
        mCheckPaintArc.setColor(mCheckBaseColor);


        mCheckArcPaint = new Paint();
        mCheckArcPaint.setAntiAlias(true);
        mCheckArcPaint.setColor(mCheckBaseColor);
        mCheckArcPaint.setStyle(Paint.Style.STROKE);
        mCheckArcPaint.setStrokeWidth(20);


        mCheckDeclinePaint = new Paint();
        mCheckDeclinePaint.setAntiAlias(true);
        mCheckDeclinePaint.setColor(Color.parseColor("#3E3E3E"));

        mUnCheckPaint = new Paint();
        mUnCheckPaint.setAntiAlias(true);
        mUnCheckPaint.setColor(mUnCheckBaseColor);
        mUnCheckPaint.setStyle(Paint.Style.STROKE);
        mUnCheckPaint.setStrokeWidth(20);

        mCheckTickPaint = new Paint();
        mCheckTickPaint.setAntiAlias(true);
        mCheckTickPaint.setColor(mCheckTickColor);
        mCheckTickPaint.setStyle(Paint.Style.STROKE);
        mCheckTickPaint.setStrokeWidth(20);


    }

    /**
     * 重置
     */
    private void reset() {
        mRingCounter = 0;
        mCircleCounter = 0;
        mAlphaCount = 0;
        scaleCounter = 50;
        mCheckArcPaint.setStrokeWidth(20); //画笔宽度重置
        postInvalidate();
    }

    /**
     * dp转px
     *
     * @param dpValue
     * @return
     */
    public int dip2px(float dpValue) {
        final float scale = getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 初始化点击事件
     */
    public void setUpEvent() {
        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                isCheckd = !isCheckd;
                reset();
                if (mOnCheckedChangeListener != null) {
                    //此处回调
                    mOnCheckedChangeListener.onCheckedChanged((CustomTickView) view, isCheckd);
                }
            }
        });
    }

    private OnCheckedChangeListener mOnCheckedChangeListener;

    public interface OnCheckedChangeListener {
        void onCheckedChanged(CustomTickView tickView, boolean isCheckd);
    }

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

}

activity_main.java

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.custom.customtickview.CustomTickView
        android:id="@+id/customTickView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:check_base_color="@color/mis"
        app:check_tick_color="@color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.378"
        app:uncheck_base_color="@color/gray"
        app:uncheck_tick_color="@color/gray">

    </com.custom.customtickview.CustomTickView>

    <TextView
        android:id="@+id/tv_show"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:width="100dp"
        android:gravity="center"
        android:text="未完成签到"
        android:textSize="20dp"
        android:textStyle="bold"
        android:textColor="@color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/customTickView"
        app:layout_constraintVertical_bias="0.099">

    </TextView>

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java

 public class MainActivity extends AppCompatActivity {
    
        private CustomTickView customTickView;
        private TextView tv_show;
    
        @SuppressLint("ObsoleteSdkInt")
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                Window window = getWindow();
                window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                        WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            }
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            tv_show = findViewById(R.id.tv_show);
            customTickView = findViewById(R.id.customTickView);
            customTickView.setUpEvent();//运行动画
            customTickView.setOnCheckedChangeListener(new CustomTickView.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CustomTickView tickView, boolean isCheckd) {
                    if(!isCheckd){
                        tv_show.setText("未完成签到");
                    }else{
                        tv_show.setText("已签到");
                    }
    
                }
            });
    
        }
    }

附上项目源码:欢迎star

项目源码

猜你喜欢

转载自blog.csdn.net/qq_35416214/article/details/105138073