先看效果图:
这里,我没有添加打钩的图片,而是单纯的用canvas来实现动画效果
中间的钩,我用了路径Path来进行描绘并实现它的动画效果。首先,这个钩由两条线段,三个顶点组成的,其实将这三个顶点作为参数传入Path对象中的lineTo()方法,再调用一下canvas.drawPath(),我们就可以得到图中这个钩的样式了
然后说说动画效果的实现,postInvalidateDelay()这个方法就很重要了,它能让onDraw()方法每隔一段时间被调用一次
所以,外部的圆环我们可以用drawArc()绘制圆弧的方式实现,每次调用onDraw()方法时绘制一定的角度,直到绘制出完整的圆环,就产生以上图中的动画效果
绘制圆环代码如下:
private int perAngle=5,currentAngle=0; //每次绘制的角度;当前已经绘制的角度
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//......
//防止绘制的角度超过360度
if(currentAngle+perAngle>360)currentAngle=360;
else currentAngle+=perAngle;
canvas.drawArc(circleRectF,0,currentAngle,false,circlePaint);
//......
postInvalidateDelayed(5);
}
最后是钩的动画实现,我首先计算了两条线段的斜率和截距:
//提供打钩的三个坐标点(radius/2,radius),(radius,(radius*3)/2),((radius*3)/2,radius/2)
pathX=new int[]{radius/2,radius,(radius*3)/2};
pathY=new int[]{radius,(radius*3)/2,radius/2};
slopes=new float[pathX.length-1];
intercepts=new float[pathX.length-1];
//根据上面三个坐标点,得到两条线段,计算斜率和截距。
for(int i=0;i<slopes.length;i++)
slopes[i]=(float)(pathY[i+1]-pathY[i]*1.0)/(pathX[i+1]-pathX[i]);
for(int i=0;i<intercepts.length;i++)
intercepts[i]=pathY[i+1]-slopes[i]*pathX[i+1];
pathX,pathY (int[]) 记录三个顶点的坐标
slopes (float[]) 记录两条线段的斜率
intercepts (float[]) 记录两条线段的截距
然后,我们设想一下,钩的移动方向一直是向右边的,也就是x轴的正方向,所以,我们从第一个顶点出发,每次调用onDraw()方法时,沿着上面的两个线段往右跑,即每次让x坐标增大一定的值,再通过y=ax+b,就可以计算出相应的y值。将(x,y)存入Path路径中,最后通过canvas.drawPath()将新保存的路径画出来,就可以实现动画效果
具体代码如下:
private int moveX=5,currentMoveX; //钩每次绘制的长度;目前移动到的位置
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//......
//绘制完圆环后,绘制打钩
//每次向x轴的正方向移动moveX的距离来实现动画效果
if(currentAngle>=360){
//防止绘制时出边界
if(currentMoveX+moveX>pathX[pathX.length-1])currentMoveX=pathX[pathX.length-1];
else currentMoveX+=moveX;
//判断目前已经绘制到哪一条线段上
for(int i=1;i<pathX.length;i++){
if(currentMoveX<=pathX[i]){
path.lineTo(currentMoveX,currentMoveX*slopes[i-1]+intercepts[i-1]);
break;
}
}
canvas.drawPath(path,pathPaint);
}
//打钩还未绘制完,调用延时重绘的方法
if(currentMoveX<pathX[pathX.length-1])
postInvalidateDelayed(5);
//......
}
下面是自定义View的完整代码:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class MyProgressFinishView extends View {
private Paint circlePaint; //环的画笔
private RectF circleRectF; //环的外切矩形
private Path path; //钩的路径
private Paint pathPaint; //钩的画笔
private int radius=200; //环的半径
private int perAngle=5,currentAngle=0; //每次绘制的角度;当前已经绘制的角度
private int[] pathX,pathY;
private float[] slopes; //每条直线的斜率
private float[] intercepts; //每条直线的截距
private int moveX=5,currentMoveX; //钩每次绘制的长度;目前移动到的位置
private boolean isFirstMeasure=true;
public MyProgressFinishView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
}
//初始化画笔
private void initPaint(){
circlePaint=new Paint();
circlePaint.setColor(Color.BLUE);
circlePaint.setStrokeWidth(8);
circlePaint.setStyle(Paint.Style.STROKE);
pathPaint=new Paint();
pathPaint.setColor(Color.BLUE);
pathPaint.setStrokeWidth(10);
pathPaint.setStyle(Paint.Style.STROKE);
}
//初始化钩的路径
private void initPath(){
circleRectF=new RectF(circlePaint.getStrokeWidth(),circlePaint.getStrokeWidth(),radius*2,radius*2);
path=new Path();
//提供打钩的三个坐标点(radius/2,radius),(radius,(radius*3)/2),((radius*3)/2,radius/2)
pathX=new int[]{radius/2,radius,(radius*3)/2};
pathY=new int[]{radius,(radius*3)/2,radius/2};
slopes=new float[pathX.length-1];
intercepts=new float[pathX.length-1];
//根据上面三个坐标点,得到两条线段,计算斜率和截距。
for(int i=0;i<slopes.length;i++)
slopes[i]=(float)(pathY[i+1]-pathY[i]*1.0)/(pathX[i+1]-pathX[i]);
for(int i=0;i<intercepts.length;i++)
intercepts[i]=pathY[i+1]-slopes[i]*pathX[i+1];
currentMoveX=pathX[0];
path.moveTo(pathX[0],pathY[0]);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int modeX=MeasureSpec.getMode(widthMeasureSpec);
int modeY=MeasureSpec.getMode(heightMeasureSpec);
int width=MeasureSpec.getSize(widthMeasureSpec);
int height=MeasureSpec.getSize(heightMeasureSpec);
//如果测量的宽高模式是固定值,让半径等于宽的值或高的值
if(modeX==MeasureSpec.EXACTLY && modeY==MeasureSpec.EXACTLY){
radius=width<height?width/2:height/2;
}else if(modeX==MeasureSpec.EXACTLY){
radius=width/2;
}else if(modeY==MeasureSpec.EXACTLY){
radius=height/2;
}
if(isFirstMeasure){
initPath();
isFirstMeasure=false;
}
//保存测量宽度和测量高度
setMeasuredDimension((int)(radius+circlePaint.getStrokeWidth())*2, (int)(radius+circlePaint.getStrokeWidth())*2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//防止绘制的角度超过360度
if(currentAngle+perAngle>360)currentAngle=360;
else currentAngle+=perAngle;
canvas.drawArc(circleRectF,0,currentAngle,false,circlePaint);
//绘制完圆环后,绘制打钩
//每次向x轴的正方向移动moveX的距离来实现动画效果
if(currentAngle>=360){
//防止绘制时出边界
if(currentMoveX+moveX>pathX[pathX.length-1])currentMoveX=pathX[pathX.length-1];
else currentMoveX+=moveX;
//判断目前已经绘制到哪一条线段上
for(int i=1;i<pathX.length;i++){
if(currentMoveX<=pathX[i]){
path.lineTo(currentMoveX,currentMoveX*slopes[i-1]+intercepts[i-1]);
break;
}
}
canvas.drawPath(path,pathPaint);
}
//打钩还未绘制完,调用延时重绘的方法
if(currentMoveX<pathX[pathX.length-1])
postInvalidateDelayed(5);
}
}
xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<com.hualinfo.myviewtext.MyProgressFinishView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"/>
</RelativeLayout>