Android 自定义View实现打钩(签到)的动画

先看效果图:

这里,我没有添加打钩的图片,而是单纯的用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>

猜你喜欢

转载自blog.csdn.net/zz51233273/article/details/107670430