凉山州规划和建设局网站十大互联网广告公司
效果图

实现原理
我们看实现的动画效果,其实是分为
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);}
测量布局
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(mCustomSize, mCustomSize);//正方形布局长宽一致}
准备工作完成,接下来我们按照刚才的步骤一步一步实现
1. 绘制未选中状态图形(圆弧和对号)
@Overrideprotected void onDraw(Canvas canvas) {if (mCustomSize > 0) {if (!isCheckd) {canvas.drawCircle(mCenter, mCenter, mRadius, mUnCheckPaint);//未选中状态的圆canvas.drawLines(mPoints, mUnCheckPaint);return;}} }
2. 绘制选中状态圆弧的旋转的动画
@Overrideprotected void onDraw(Canvas canvas) {//省略以上代码mRingCounter += 10;//按照如下速率转动if (mRingCounter >= 360) {mRingCounter = 360;}canvas.drawArc(mRectF, 90, mRingCounter, false, mCheckArcPaint);}
3. 绘制选中状态圆弧向中心收缩铺满动画
这里向中心收缩的动画我们可以逆向思维
先绘制指定颜色的整体圆,然后在指定颜色整体圆的图层上在绘制一个背景色的圆(半径不断缩小) 半径不断缩小,背景就不断露出来,达到向中心收缩的效果。
@Overrideprotected void onDraw(Canvas canvas) {//省略以上代码if (mRingCounter == 360) {//先绘制指定颜色的圆canvas.drawCircle(mCenter, mCenter, mRadius, mCheckPaint);//然后在指定颜色的图层上,再绘制背景色的圆(半径不断缩小) 半径不断缩小,背景就不断露出来,达到向中心收缩的效果mCircleCounter += 10;canvas.drawCircle(mCenter, mCenter, mRadius - mCircleCounter, mCheckDeclinePaint); }
4. 绘制选中状态对号
我们让对号出现的有延迟效果并加上透明出现的效果
@Overrideprotected void onDraw(Canvas canvas) {//省略以上代码if (mCircleCounter >= mRadius + 100) {//做延迟效果mAlphaCount += 20;if (mAlphaCount >= 255) mAlphaCount = 255; //显示对号(外加一个透明的渐变)mCheckTickPaint.setAlpha(mAlphaCount);//设置透明度//画白色的对号canvas.drawLines(mPoints, mCheckTickPaint);}}
5. 绘制选中状态下圆的放大回弹动画
这里我们的实现思路是在整体圆图层上在画一个圆弧,圆弧的宽度由增大到缩小最后直至为0,这样达到放大回弹的效果
@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic void onCheckedChanged(CustomTickView tickView, boolean isCheckd) {if(!isCheckd){tv_show.setText("未完成签到");}else{tv_show.setText("已签到");}}});
完整代码
CustomTickView.java
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);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(mCustomSize, mCustomSize);}@Overrideprotected 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() {@Overridepublic 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.xml
<?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.CustomTickViewandroid: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><TextViewandroid: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")@Overrideprotected 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() {@Overridepublic void onCheckedChanged(CustomTickView tickView, boolean isCheckd) {if(!isCheckd){tv_show.setText("未完成签到");}else{tv_show.setText("已签到");}}});}}
源码地址
https://gitee.com/Mkingm/CustomTickView