Android绘制波浪线 进阶

本文介绍在Android中绘制可自定义长度的波浪线视图,通过贝塞尔曲线和Shader实现波浪线的动态效果,同时展示了两种实现方法:使用BitmapShader和画布裁剪。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

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

拓展:

我们可以有两种方式实现这个效果,一个简单点,一个复杂点,先说说复杂的。


方式一

使用方式一,我们需要准备两个知识点

  1. Android绘制波浪线
  2. Android之Shader完全理解指南

在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个技巧:

  1. 使用贝塞尔曲线绘制波浪线
  2. 在可视范围之外再加一条相同形状的波浪线,反复平移波浪线造成波浪假象
  3. 将波浪绘制在bitmap上,将bitmap作为一个shader赋予paint
  4. 使用该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();

    }

}

总结

方式二相对与第一种方法就简单很多了,这里就只写核心吧

  1. 将画布裁剪成自己想要的形状
  2. 使用贝塞尔曲线绘制波浪线

主要技巧就这2点,相信你们已经理解两种绘制波浪线的方式了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值