今天让我们来绘制一条波浪线,并且提供了控制波浪大小和波浪速度的方法,首先我们来看看完成的效果:
效果是不是还不错,接下来我们就来看看具体怎么实现的吧!
实现思路
波浪线的绘制
波浪线是怎么实现的呢,其实这用到的内塞尔曲线。
大家可以在这里看看贝塞尔曲线的使用方式。——贝塞尔曲线开发的艺术
使用一阶贝塞尔曲线画出前半截凸起的曲线,再使用一阶贝塞尔曲线画出后半截的贝塞尔曲线。
具体实现方式,为了看着简单点,我就直接写数字了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.moveTo(0, 500);
path.quadTo(screenWidth / 4, 300, screenWidth / 2, 500);
path.moveTo(screenWidth / 2, 500);
path.quadTo(screenWidth / 4 * 3, 700, screenWidth, 500);
canvas.drawPath(path, paint);
}
效果大概是这个样子:
波浪的动态
大家有没有觉得这个波浪线很像y=sinx
这个函数图像,只要y=sin(x+a),a是一个随着时间无限增大的值,该函数图像就可以动起来,大家可以在这个网站上试试——Desmos图形计算器
大概要输入一个这样的函数:y=sin(x/5+a),a做为一个滑块。这样大家就可以看到波浪线的动态图了。
好了,点子已经有了,只要我们把那个波浪线向后移动,就可以看出波浪的动态效果,于是我们做出了如下代码:
private int screenWidth;
private float xoffset = 0;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
screenWidth = wm.getDefaultDisplay().getWidth();
ValueAnimator animator = new ValueAnimator();
animator.setFloatValues(0, screenWidth);
animator.setDuration(3000);
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
xoffset = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.moveTo(0 + xoffset, 500);
path.quadTo(screenWidth / 4 + xoffset, 300, screenWidth / 2 + xoffset, 500);
path.moveTo(screenWidth / 2 + xoffset, 500);
path.quadTo(screenWidth / 4 * 3 + xoffset, 700, screenWidth + xoffset, 500);
canvas.drawPath(path, paint);
}
效果如下:
。。。
好吧,确实,这确实没有想象中的波浪效果,而且还有点——操蛋。。
我也没有说这是最终效果啊,我只是说明了一下动态效果,那么要怎么才能让波浪动的毫无破绽呢,其实我们可以。。。
在屏幕外面再画一个波浪,这样动起来就没有破绽了。
于是有了以下代码:
private int screenWidth;
private float xoffset = 0;
private Paint paint;
private Path path;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
screenWidth = wm.getDefaultDisplay().getWidth();
paint = new Paint();
paint.setColor(Color.RED);
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
path = new Path();
ValueAnimator animator = new ValueAnimator();
animator.setFloatValues(0, screenWidth);
animator.setDuration(3000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
xoffset = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
path.moveTo(0 + xoffset, 500);
path.quadTo(screenWidth / 4 + xoffset, 300, screenWidth / 2 + xoffset, 500);
path.moveTo(screenWidth / 2 + xoffset, 500);
path.quadTo(screenWidth / 4 * 3 + xoffset, 700, screenWidth + xoffset, 500);
path.moveTo(0 + xoffset - screenWidth, 500);
path.quadTo(screenWidth / 4 + xoffset - screenWidth, 300, screenWidth / 2 + xoffset - screenWidth, 500);
path.moveTo(screenWidth / 2 + xoffset - screenWidth, 500);
path.quadTo(screenWidth / 4 * 3 + xoffset - screenWidth, 700, screenWidth + xoffset - screenWidth, 500);
canvas.drawPath(path, paint);
}
我们再来看看效果图:
哎呦,不错喔,是不是有点意思了,接下来我们只要做点细节上的处理,按照一个正常的自定义view的流程走,我们最终会得到如下代码:
public class MyWaveView extends View {
private Paint mPaint;
private Path mPath;
// view宽度
private int width;
// view高度
private int height;
// 波浪高低偏移量
private int offset = 20;
// X轴,view的偏移量
private int xoffset = 0;
// view的Y轴高度
private int viewY = 0;
// 波浪速度
private int waveSpeed = 50;
private ValueAnimator animator;
public MyWaveView(Context context) {
super(context);
init(context);
}
public MyWaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(5);
mPath = new Path();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
animator = new ValueAnimator();
animator.setFloatValues(0, width);
animator.setDuration(waveSpeed * 20);
animator.setRepeatCount(-1);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float change = (float) animation.getAnimatedValue();
xoffset = (int) change;
invalidate();
}
});
animator.start();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//设置一个默认值,就是这个View的默认宽度为500,这个看我们自定义View的要求
int result = 500;
if (specMode == MeasureSpec.AT_MOST) {//相当于我们设置为wrap_content
result = specSize;
} else if (specMode == MeasureSpec.EXACTLY) {//相当于我们设置为match_parent或者为一个具体的值
result = specSize;
}
width = result;
return result;
}
private int measureHeight(int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
int result = 500;
if (specMode == MeasureSpec.AT_MOST) {
result = specSize;
} else if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
}
height = specSize;
return result;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
viewY = height / 2;
// 绘制屏幕内的波浪
mPath.moveTo(xoffset, viewY);
mPath.quadTo(width / 4 + xoffset, viewY - offset, width / 2 + xoffset, viewY);
mPath.moveTo(width / 2 + xoffset, viewY);
mPath.quadTo(width / 4 * 3 + xoffset, viewY + offset, width + xoffset, viewY);
// 绘制屏幕外的波浪
mPath.moveTo(xoffset - width, viewY);
mPath.quadTo(width / 4 + xoffset - width, viewY - offset, width / 2 + xoffset - width, viewY);
mPath.moveTo(width / 2 + xoffset - width, viewY);
mPath.quadTo(width / 4 * 3 + xoffset - width, viewY + offset, width + xoffset - width, viewY);
canvas.drawPath(mPath, mPaint);
}
/**
* 设置 波浪的高度
*/
public void setWaveHeight(int waveHeight){
offset = waveHeight;
}
/**
* 获取 波浪的高度
*/
public int getWaveHeight(){
return offset;
}
/**
* 设置 波浪的速度
*/
public void setWaveSpeed(int speed){
waveSpeed = 2000 - speed * 20;
animator.setDuration(waveSpeed);
}
/**
* 获取 波浪的速度
*/
public int getWaveSpeed(){
return waveSpeed;
}
}
好了,大概实现一条波浪线的流程大概就是这样。
总结
使用贝塞尔曲线做出波浪线,使用属性动画让波浪线动起来
连续原理:
最开始是这样的:
移动后:
当移动到图二的时候,该动画已经结束了,但由于该动画指定了无限循环,所以实际上又会回到图一的形状。这样解释可能不太直观,所以:
好了,这下各位看官看的够清楚吧,里面的黑框就是我们的手机屏幕大小,这样给我们造成一种无限波动的假象,大家可以试试用两只手挡住黑框两边,能看到波动效果哦!