I will see you in another life, when we are both cats. —— 《Vanilla Sky》
不想学习想要娱乐的时候忽然看到了这个动画效果,出于程序员的本能,就仿出来了。
闲话不多说,开整。
实现原理
我们看实现的动画效果,其实是分为
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("已签到");
}
}
});
}
}