前言
在我的上一篇博客中,我讲解了如何绘制一条波浪线,今天我们的目的是,绘制一条随心所欲的波浪线,什么叫随心所欲的波浪线呢,由于上篇主要讲解的是绘制波浪线的原理,不过就有人提问了,万一我想要的波浪线长度小于屏幕长度怎么办,那么今天我们就来讲解下,怎么绘制一条长度可以自己把控的波浪线,先看看效果图吧。

拓展:
我们可以有两种方式实现这个效果,一个简单点,一个复杂点,先说说复杂的。
方式一
使用方式一,我们需要准备两个知识点
在Activity中绘制图形
在实现这个波浪线之前,我们需要知道一件事情,在Android中,除了通过自定义view的时候,我们会用到canvas,其他地方好像就见不到canvas了,现在我们先做一件事情,不在onDraw中使用canvas,在activity中使用canvas,怎么实现呢。
主要还是用到canvas的构造方法,我们可以使用canvas的Canvas canvas = new Canvas(bitmap);
构造方法,将图像绘制在一张bitmap上面,然后将这个bitmap设置给ImageView,这个bitmap既可以是正常的普通的bitmap,也可以是我们通过代码得到的一张空白的bitmap,那么直接看代码吧。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 创建空白的bitmap作为画布
Bitmap bitmap = Bitmap.createBitmap(1080, 1920, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
canvas.drawRect(new RectF(100,100,300,300),paint);
ImageView imageView = findViewById(R.id.imageview);
imageView.setImageBitmap(bitmap);
}
}
效果也很简单
设置shader
现在我们可以跳出自定义view中的onDraw方法了,即便不在onDraw中,我们照样可以绘制图形。
那么,我们首先通过不借助onDraw的方式,绘制一条波浪线,再将这个bitmap作为paint的shader,然后使用这个paint去绘制一个矩形,等等,效果好像已经实现了。
先上代码吧
public class WaveView extends View {
private Paint mPaint;
// 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 WaveView(Context context) {
super(context);
init(context);
}
public WaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mPaint = new Paint();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
height = wm.getDefaultDisplay().getHeight();
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;
createShader();
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);
canvas.drawRect(new Rect(200, 0, width - 200, height), mPaint);
}
private void createShader() {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
Path path = new Path();
viewY = height / 2;
// 绘制屏幕外的波浪
path.moveTo(xoffset - width, viewY);
path.quadTo(width / 4 + xoffset - width, viewY - offset, width / 2 + xoffset - width, viewY);
path.moveTo(width / 2 + xoffset - width, viewY);
path.quadTo(width / 4 * 3 + xoffset - width, viewY + offset, width + xoffset - width, viewY);
// 绘制屏幕内的波浪
path.moveTo(xoffset, viewY);
path.quadTo(width / 4 + xoffset, viewY - offset, width / 2 + xoffset, viewY);
path.moveTo(width / 2 + xoffset, viewY);
path.quadTo(width / 4 * 3 + xoffset, viewY + offset, width + xoffset, viewY);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, paint);
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(bitmapShader);
}
}
布局中,该控件的高度我设置的是100dp
效果图:
拓展的实现方式就更简单了,直接:
public class WaveView extends View {
private Paint mPaint;
// 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 WaveView(Context context) {
super(context);
init(context);
}
public WaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mPaint = new Paint();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
height = wm.getDefaultDisplay().getHeight();
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;
createShader();
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);
canvas.drawRect(new Rect(200, 0, width - 200, height), mPaint);
}
private void createShader() {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
Path path = new Path();
viewY = height / 2;
// 绘制屏幕外的波浪
path.moveTo(xoffset - width, viewY);
path.quadTo(width / 4 + xoffset - width, viewY - offset, width / 2 + xoffset - width, viewY);
path.moveTo(width / 2 + xoffset - width, viewY);
path.quadTo(width / 4 * 3 + xoffset - width, viewY + offset, width + xoffset - width, viewY);
// 绘制屏幕内的波浪
path.moveTo(xoffset, viewY);
path.quadTo(width / 4 + xoffset, viewY - offset, width / 2 + xoffset, viewY);
path.moveTo(width / 2 + xoffset, viewY);
path.quadTo(width / 4 * 3 + xoffset, viewY + offset, width + xoffset, viewY);
// 新增了这里
path.lineTo(width + xoffset, height);
path.lineTo(xoffset - width, height);
path.lineTo(xoffset - width, viewY);
canvas.drawPath(path, paint);
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(bitmapShader);
}
}
效果图:
原理
还是来段原理吧,上图直接了解原理,跟上篇文章的原理一模一样。
首先先看线段的波浪
其中黑框内代表可视区域,这里的黑框的宽度不是手机的屏幕宽度,通过底层shader的作怪,让手机屏幕上只显示我们希望让用户看到的宽度,这里也一样,用双手遮住可以看到波浪效果。
我们再显示下拓展的原理
这两幅图中,高亮的区域就是用户所看到的形状了。
总结
绘制波浪线,主要用到了以下4个技巧:
- 使用贝塞尔曲线绘制波浪线
- 在可视范围之外再加一条相同形状的波浪线,反复平移波浪线造成波浪假象
- 将波浪绘制在bitmap上,将bitmap作为一个shader赋予paint
- 使用该paint绘制形状
主要技巧就这4点吧,其他应该就没有了。
方式二
该方法主要是用到了画布的clip有关的方法,该方法主要的作用是可以裁剪画布,比如说将画布裁剪成一个圆,那么我们绘制的东西,就只能在这个圆上面了。
效果图:
那么先讲讲和画布裁剪有关的clip系列的方法,就顺带一提吧,这个东西不难理解,其实原理我们能直接这样理解。如图:

画布的其他地方被剪掉了,可绘制的区域就只有这个圆圈里面。
canvas的clip系列的方法主要也就2种:
canvas.clipRect(int left, int top, int right, int bottom);
canvas.clipPath(Path path);
不用我细说,你看名字也知道这个是什么意思了吧,一个是裁剪成矩形,图像只能绘制在这个矩形上,另一个是path,说明你可以裁剪成各种形状,圆形三角形或者是其他稀奇古怪的形状,就随你高兴了。
在使用clip系列的方法之间,推荐先使用canvas.save()
,保存当前的画布状态,然后裁剪绘制完毕后,再使用canvas.restore()
把画布还原成裁剪前的状态,当然,还原到之前的状态不会清除裁剪后绘制的图形,绘制完毕后恢复成一个完整画布的好处我想不用我细说你也知道,当然是让后续的绘制不要受裁剪后画布大小的限制啦!
所以完整的代码就是这个样子的:
public class WaveView extends View {
private Paint paint;
private Path path;
// 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 WaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
path = new Path();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
height = wm.getDefaultDisplay().getHeight();
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);
// 保存画布当前状态
canvas.save();
// 裁剪画布
Path clipPath = new Path();
clipPath.addCircle(540,850,300, Path.Direction.CW);
canvas.clipPath(clipPath);
path.reset();
viewY = height / 2;
// 绘制屏幕外的波浪
path.moveTo(xoffset - width, viewY);
path.quadTo(width / 4 + xoffset - width, viewY - offset, width / 2 + xoffset - width, viewY);
path.moveTo(width / 2 + xoffset - width, viewY);
path.quadTo(width / 4 * 3 + xoffset - width, viewY + offset, width + xoffset - width, viewY);
// 绘制屏幕内的波浪
path.moveTo(xoffset, viewY);
path.quadTo(width / 4 + xoffset, viewY - offset, width / 2 + xoffset, viewY);
path.moveTo(width / 2 + xoffset, viewY);
path.quadTo(width / 4 * 3 + xoffset, viewY + offset, width + xoffset, viewY);
// 新增了这里
path.lineTo(width + xoffset, height);
path.lineTo(xoffset - width, height);
path.lineTo(xoffset - width, viewY);
canvas.drawPath(path, paint);
// 将画布还原成裁剪前的状态
canvas.restore();
}
}
总结
方式二相对与第一种方法就简单很多了,这里就只写核心吧
- 将画布裁剪成自己想要的形状
- 使用贝塞尔曲线绘制波浪线
主要技巧就这2点,相信你们已经理解两种绘制波浪线的方式了。