先看效果图:
下面我们来研究研究它是如何实现的。为了方便我们观察,再来看下面这张动图:
有感受到些什么吗?其实,我们需要做的是同时创建多条贝塞尔曲线,然后移动每条曲线上的起点和终点就能实现波浪效果了。
例如在上面图中,黑色的点是贝塞尔曲线上的起点与终点,蓝色的点是贝塞尔曲线的控制点。控制点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>