码农翻身

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

- by MRyan, 2020-03-27


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

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

实现原理

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

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

项目源码

作者:MRyan


本文采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
转载时请注明本文出处及文章链接。本文链接:https://wormholestack.com/archives/633/
2025 © MRyan 154 ms