我尽量不打错别字,用词准确,不造成阅读障碍。
本文是完成一个水波流动的加载控件,网上有很多写法,这里写一个比较简单的。本人喜欢在代码中写注释,核心部分都在注释里
效果图:
首先先分析一下,需要几个画笔;默认是矩形背景,所以我们需要把背景“截”成圆形的,这就需要一个画笔,然后是画波浪线,即贝塞尔曲线,又需要一个画笔,最后是写文字,需要一个画笔;好,总共需要三个画笔;颜色啊、抗锯齿啊、填充啊等都要设置的,然后分析一下画波浪,首先需要一个path对象,这样方便移动起点坐标位置,用moveTo()方法移动起点坐标,然后用quadTo()方法画正弦曲线,这个方法有四个参数,代表两个坐标点,即quadTo(x1,y1,x2,y2)第一个坐标表示控制点坐标,第二个坐标表示结束点坐标,没有起点坐标,起点坐标就是画笔调用方法时所在的坐标位置,控制点坐标贝塞尔中的点,结束点坐标就是落在X轴上的坐标;图示:
代码如下:
public class WaterWaveView extends View {
private int width;
private int height;
private Point startPoint; //波浪起始点
private Path path; //波浪路径
private Path circlePath; //外圆路径
private Paint paint; //波浪画笔
private Paint circlePaint; //外圆画笔
private Paint textPaint; //文字画笔
private int waveHeight = 340; //振幅高度
private int cycle = 160; //1/4周期
private int translateX = 40;
public WaterWaveView(Context context) {
super(context);
}
public WaterWaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public WaterWaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getViewSize(400, widthMeasureSpec);
height = getViewSize(400, widthMeasureSpec);
}
private int getViewSize(int defaultSize, int measureSpec) {
int viewSize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
viewSize = defaultSize;
break;
case MeasureSpec.AT_MOST:
viewSize = size;
break;
case MeasureSpec.EXACTLY:
viewSize = size;
break;
}
return viewSize;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startPoint = new Point(0, height / 2);
path = new Path();
circlePath = new Path();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
circlePaint = new Paint();
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setAntiAlias(true);
circlePaint.setColor(Color.parseColor("#FF4081"));
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.BLACK);
textPaint.setTextSize(32);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.moveTo(startPoint.x, startPoint.y);
int j = 1;
//画正弦
for (int i = 1; i <= 4; i++) {
if (i % 2 == 0) {
path.quadTo(startPoint.x + (cycle * j), startPoint.y + waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
} else {
path.quadTo(startPoint.x + (cycle * j), startPoint.y - waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
}
j += 2;
}
path.lineTo(width, height);
path.lineTo(startPoint.x, height);
path.lineTo(startPoint.x, startPoint.y);
path.close();
canvas.drawPath(path, paint);
path.reset(); //路径重置
}
}
结果是这样的:
代码中画正弦的方法有很多种,找了一种比较简单的,但是时间复杂度较高。接下来就是圆形外表和滚动了。先说滚动,想法就是每隔150毫秒刷新界面,每次刷新起始坐标向右移动一段距离,然后继续画正弦,就会有波动的效果。代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.moveTo(startPoint.x, startPoint.y);
int j = 1;
//画正弦
for (int i = 1; i <= 4; i++) {
if (i % 2 == 0) {
path.quadTo(startPoint.x + (cycle * j), startPoint.y + waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
} else {
path.quadTo(startPoint.x + (cycle * j), startPoint.y - waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
}
j += 2;
}
path.lineTo(width, height);
path.lineTo(startPoint.x, height);
path.lineTo(startPoint.x, startPoint.y);
path.close();
canvas.drawPath(path, paint);
//起始坐标改动
if (startPoint.x + translateX >= 0) {
startPoint.x = -cycle * 4;
}
startPoint.x += translateX;
path.reset();
//刷新界面
postInvalidateDelayed(mNewWaveSpeed);
}
效果是这样的:
最后就剩一个“截”圆形图了,很简单。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
circlePath.addCircle(width / 2, height / 2, width / 2, Path.Direction.CW);
canvas.clipPath(circlePath);
canvas.drawPath(circlePath, circlePaint);
circlePath.reset();
path.moveTo(startPoint.x, startPoint.y);
int j = 1;
//画正弦
for (int i = 1; i <= 4; i++) {
if (i % 2 == 0) {
path.quadTo(startPoint.x + (cycle * j), startPoint.y + waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
} else {
path.quadTo(startPoint.x + (cycle * j), startPoint.y - waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
}
j += 2;
}
path.lineTo(width, height);
path.lineTo(startPoint.x, height);
path.lineTo(startPoint.x, startPoint.y);
path.close();
canvas.drawPath(path, paint);
//起始坐标改动
if (startPoint.x + translateX >= 0) {
startPoint.x = -cycle * 4;
}
startPoint.x += translateX;
path.reset();
//刷新界面
postInvalidateDelayed(mNewWaveSpeed);
}
这样就达到了开篇所演示的效果,我就不截图了。最后的最后,增加一些显示文字的功能,并添加设置文字的set方法等等,自定义属性写入attrs文件中,可以设置波浪颜色,波浪振幅,文字颜色、大小等等,在此不叙述了,还可以根据progress的大小设置Y轴高度,达到进度的展示效果,给个最后的代码,不全:
public class WaterWaveView extends View {
private int width;
private int height;
private Point startPoint; //波浪起始点
private Path path; //波浪路径
private Path circlePath; //外圆路径
private Paint paint; //波浪画笔
private Paint circlePaint; //圆形画笔
private Paint textPaint; //文字画笔
private int waveHeight = 340; //振幅
private int cycle = 160; //1/4周期
private int mNewWaveSpeed = 100; //波浪滚动速度
private int translateX = 40; //位移大小
private int progress = 0;
private boolean isStart;
public WaterWaveView(Context context) {
super(context);
}
public WaterWaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public WaterWaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getViewSize(400, widthMeasureSpec);
height = getViewSize(400, widthMeasureSpec);
}
private int getViewSize(int defaultSize, int measureSpec) {
int viewSize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
viewSize = defaultSize;
break;
case MeasureSpec.AT_MOST:
viewSize = size;
break;
case MeasureSpec.EXACTLY:
viewSize = size;
break;
}
return viewSize;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startPoint = new Point(-cycle * 3, height / 2);
startPoint = new Point(0, height / 2);
path = new Path();
circlePath = new Path();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
circlePaint = new Paint();
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setAntiAlias(true);
circlePaint.setColor(Color.parseColor("#FF4081"));
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.BLACK);
textPaint.setTextSize(32);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
circlePath.addCircle(width / 2, height / 2, width / 2, Path.Direction.CW);
canvas.clipPath(circlePath);
canvas.drawPath(circlePath, circlePaint);
circlePath.reset();
startPoint.y = (int) (height - (progress / 100.0 * height));
path.moveTo(startPoint.x, startPoint.y);
//画正弦
int j = 1;
for (int i = 1; i <= 8; i++) {
if (i % 2 == 0) {
path.quadTo(startPoint.x + (cycle * j), startPoint.y + waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
} else {
path.quadTo(startPoint.x + (cycle * j), startPoint.y - waveHeight, startPoint.x + (cycle * 2) * i, startPoint.y);
}
j += 2;
}
path.lineTo(width, height);
path.lineTo(startPoint.x, height);
path.lineTo(startPoint.x, startPoint.y);
path.close();
canvas.drawPath(path, paint);
//写字
canvas.drawText(progress + "%", width / 2, height / 2, textPaint);
if (progress == 100) {
progress = 0;
} else {
progress++;
}
//起始坐标改动
if (startPoint.x + translateX >= 0) {
startPoint.x = -cycle * 4;
}
startPoint.x += translateX;
path.reset();
if (isStart) {
postInvalidateDelayed(mNewWaveSpeed);
}
}
public void setProgress(int progress) {
if (progress > 100 || progress < 0) {
throw new RuntimeException(getClass().getName() + "进度值须在[0,100]");
}
this.progress = progress;
invalidate();
}
//开始动画
public void start() {
isStart = true;
}
//停止动画
public void stop() {
isStart = false;
}
问题:
主要是性能问题,发现动画一直执行,内存会缓慢增加,每次增加0.01MB左右,动画会缓慢变卡,但是总体时间比较长才能看出来。
github地址,里面有许多自定义View
https://github.com/longlong-2l/MySelfViewDemo