效果图
实现原理
我们看实现的动画效果,其实是分为
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
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.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.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("已签到");
}
}
});
}
}
源码地址
https://gitee.com/Mkingm/CustomTickView