Android 自定义View贝塞尔曲线实现波浪动画

先看效果图:

下面我们来研究研究它是如何实现的。为了方便我们观察,再来看下面这张动图:

有感受到些什么吗?其实,我们需要做的是同时创建多条贝塞尔曲线,然后移动每条曲线上的起点和终点就能实现波浪效果了。

例如在上面图中,黑色的点是贝塞尔曲线上的起点与终点,蓝色的点是贝塞尔曲线的控制点。控制点x的坐标其实是由起点和终点决定的。控制点x坐标计算公式是:x=(起点的x+终点的x)/2,控制点y的坐标我们可以任意取值,从而来达到最好的效果。

所以,移动贝塞尔曲线的起点和终点后,它的控制点也会跟着移动。我们只需要关心起点和终点的移动问题。

我们再观察一下上面的动图,圆中可以同时存在两个贝塞尔曲线,另外一条曲线的初始位置是在圆的左侧,为什么要这样呢?因为如果圆外没有曲线的话,圆内曲线向右移动后,它的左侧就没有东西了。而在圆外左侧添加了曲线,圆内曲线向右移动,外部的曲线就能进入内部来弥补本应空缺的地方。

最后一点,曲线也可以从圆内移动到圆外,当曲线中的某个点移动到边界的时候,我们就要将这个点重置到最前面。这个边界到圆右侧的距离就是曲线起点到终点的长度

接下来,我们下分两步走:

  • 绘制波浪的区域
  • 绘制波浪的动画效果

首先我们绘制波浪的区域:

private MyPoint[] points=new MyPoint[controlNum+2]; //贝塞尔点的集合
private int radius=350;  //圆的半径,假设圆的左上角点的坐标为:(radius*2,radius*3)
private int marginWidth=radius*2-radius;    //圆的左侧离屏幕左侧的距离
private int marginHeight=radius*3-radius;   //圆的左侧离屏幕顶部的距离
private int controlNum=2;   //圆内控制点的数量
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //切分出波浪区域
    canvas.save();
    canvas.clipPath(getBezierPath());
    canvas.clipPath(getCirclePath());
    canvas.drawColor(Color.RED);
    canvas.restore();
    //......
}

//获取圆的区域
private Path getCirclePath(){
    circlePath.reset();
    circlePath.addCircle(radius*2,radius*3,radius, Path.Direction.CW);
    return circlePath;
}

//获取贝塞尔曲线下方的区域
private Path getBezierPath(){
    path.reset();
    path.moveTo(points[0].getX(),points[0].getY());
    for(int i=1;i<points.length;i++){
        //如果是数组下标为奇数,则贝塞尔曲线的控制点在下方
        if(i%2==1)path.quadTo((points[i].getX()+points[i-1].getX())/2,points[i].getY()+isReverse*radius/5,points[i].getX(),points[i].getY());
        //如果是数组下标为偶数,则贝塞尔曲线的控制点在上方
        else path.quadTo((points[i].getX()+points[i-1].getX())/2,points[i].getY()-isReverse*radius/5,points[i].getX(),points[i].getY());
    }
    path.lineTo(marginWidth+radius*2,marginHeight+radius*2);
    path.lineTo(marginWidth-radius*2/controlNum,marginHeight+radius*2);
    return path;
}

上面的代码用图表示是下面这样:

波浪我们有了,现在我们来制作动画效果。

改变每个贝塞尔曲线上起点和终点的x坐标:

private int isReverse=-1;   //贝塞尔曲线的控制点反转
private float moveSpeed=5f;  //波浪移动速度
//改变贝塞尔曲线的位置
private void changeBezierPos(){
    for(int i=0;i<points.length;i++){
        points[i].setX(points[i].getX()+moveSpeed);
    }
    //如果最后一个贝塞尔点到了末尾,将其重置到首位。并将数组元素重新排序
    if(points[points.length-1].getX()>=marginWidth+radius*2+radius*2/controlNum){
        points[points.length-1].setX(marginWidth-radius*2/controlNum);
        //下面为数组重排序
        MyPoint myPoint=points[points.length-1];
        for(int i=points.length-2;i>=0;i--){
            points[i+1]=points[i];
        }
        points[0]=myPoint;
        isReverse=-isReverse;
    }
}

这里要注意,每次把点重置到最前面的时候,贝塞尔曲线的控制点要进行翻转,即原本在波浪上方的变成在波浪下方

调用postInvalidateDelay()不断重绘:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //切分出波浪区域
    canvas.save();
    canvas.clipPath(getBezierPath());
    canvas.clipPath(getCirclePath());
    canvas.drawColor(Color.RED);
    canvas.restore();

    canvas.drawCircle(radius*2,radius*3,radius,paint);
    changeBezierPos();
    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.util.AttributeSet;
import android.view.View;

import com.hualinfo.bean.beizer.MyPoint;

import androidx.annotation.Nullable;

public class MyBezierProgressView extends View {
    private Paint paint;
    private Path path,circlePath;
    private int radius=350;  //圆的半径,假设圆的左上角点的坐标为:(radius*2,radius*3)
    private int marginWidth=radius*2-radius;    //圆的左侧离屏幕左侧的距离
    private int marginHeight=radius*3-radius;   //圆的左侧离屏幕顶部的距离
    private float moveSpeed=5f;  //波浪移动速度
    private int isReverse=-1;   //贝塞尔曲线的控制点反转
    private int controlNum=2;   //圆内控制点的数量
    private MyPoint[] points=new MyPoint[controlNum+2]; //贝塞尔点的集合
    public MyBezierProgressView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init(){
        path=new Path();
        circlePath=new Path();
        paint=new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(6);
        points[0]=new MyPoint(marginWidth-radius*2/controlNum,marginHeight+radius);
        for(int i=1;i<=controlNum+1;i++){
            points[i]=new MyPoint(marginWidth+(radius*2/controlNum)*(i-1),marginHeight+radius);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //切分出波浪区域
        canvas.save();
        canvas.clipPath(getBezierPath());
        canvas.clipPath(getCirclePath());
        canvas.drawColor(Color.RED);
        canvas.restore();

        canvas.drawCircle(radius*2,radius*3,radius,paint);
        changeBezierPos();
        postInvalidateDelayed(5);
    }

    //改变贝塞尔曲线的位置
    private void changeBezierPos(){
        for(int i=0;i<points.length;i++){
            points[i].setX(points[i].getX()+moveSpeed);
        }
        //如果最后一个贝塞尔点到了末尾,将其重置到首位。并将数组元素重新排序
        if(points[points.length-1].getX()>=marginWidth+radius*2+radius*2/controlNum){
            points[points.length-1].setX(marginWidth-radius*2/controlNum);
            //下面为数组重排序
            MyPoint myPoint=points[points.length-1];
            for(int i=points.length-2;i>=0;i--){
                points[i+1]=points[i];
            }
            points[0]=myPoint;
            isReverse=-isReverse;
        }
    }

    //获取圆的区域
    private Path getCirclePath(){
        circlePath.reset();
        circlePath.addCircle(radius*2,radius*3,radius, Path.Direction.CW);
        return circlePath;
    }

    //获取贝塞尔曲线下方的区域
    private Path getBezierPath(){
        path.reset();
        path.moveTo(points[0].getX(),points[0].getY());
        for(int i=1;i<points.length;i++){
            //如果是数组下标为奇数,则贝塞尔曲线的控制点在下方
            if(i%2==1)path.quadTo((points[i].getX()+points[i-1].getX())/2,points[i].getY()+isReverse*radius/5,points[i].getX(),points[i].getY());
            //如果是数组下标为偶数,则贝塞尔曲线的控制点在上方
            else path.quadTo((points[i].getX()+points[i-1].getX())/2,points[i].getY()-isReverse*radius/5,points[i].getX(),points[i].getY());
        }
        path.lineTo(marginWidth+radius*2,marginHeight+radius*2);
        path.lineTo(marginWidth-radius*2/controlNum,marginHeight+radius*2);
        return path;
    }
}

MyPoint类:

public class MyPoint {
    private float x;
    private float y;

    public MyPoint() {
    }

    public MyPoint(float x, float y) {
        this.x = x;
        this.y = y;
    }

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }
}

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.myviewtext.MyBezierProgressView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</RelativeLayout>

猜你喜欢

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