自定义view绘制波浪图 贝塞尔曲线

波浪图

先上一张效果图
在这里插入图片描述
感觉还是挺炫酷的。其中用到的技术点就是贝塞尔曲线,说到贝塞尔曲线,它能做的东西就太多了,qq未读消息气泡拖拽,波浪效果,轨迹变化的动画都可以依赖贝塞尔曲线实现。

而我这里也不是自己造轮子,而是站在巨人的肩膀上。Android已经封装好了一个方法,就是path类的quadTo方法来绘制二阶贝塞尔曲线。更多阶的咱们暂且不谈。

1、构造贝塞尔曲线

二阶贝塞尔曲线介绍

在这里插入图片描述
在这里插入图片描述
先来描述一下各个点和线的含义,上图这条红线就是我们的贝塞尔曲线,P0是起始点,P2是结束点,P1是插入进来的控制点,我们在P0P1上找一点Q0,在P1P2上找一点Q1,使得P0Q0 / P0P1 = P1Q1 / P1P2 = t,然后连接Q0Q1,在Q0Q1上找一点B,使得Q0B / Q0Q1 = t,这样在Q0从P0到P1移动的过程中,Q1,B也在不断移动,而B的移动轨迹,就是我们要的贝塞尔曲线。

贝塞尔曲线在Android中的代码实现

在绘制贝塞尔曲线的时候,有三个重要的点,即起点P0,终点P2,以及控制点P1。实现思路如下:

  1. 首先在自定义View的构造方法中初始化好Paint的基本设置(其中有三个patin,贝塞尔线,控制线,文字)
  2. 实现onSizeChanged方法,用她来确定自定义View的大小。我们可以在此方法中确定起点,终点,控制点的坐标。(这里的第一二个参数应该是控件的宽和高)
  3. 然后在onDraw方法中使用Canvas绘制贝塞尔曲线,绘制曲线就要指定Path的移动路径。首先调用reset()方法,然后利用moveTo方法设置起点P0,然后利用quadTo方法设置控制点和终点。最后调用canvas.drawPath(),传入Path和Paint
  4. 以上过程已经将贝塞尔曲线绘制完毕,为了更好的观感体验,在onDraw方法中添加绘制起点、终点、控制点的圆圈Point和文字提示,再分别绘制两条起、终点到控制点的直线。整体效果更加清晰
  5. 最后实现boolean onTouchEvent(MotionEvent event)方法,监听MotionEvent.ACTION_MOVE事件,将手指一动坐标赋值给控制点,调用invalidate();重新渲染。这样便可动态绘制二阶贝塞尔曲线。

来贴一下代码:

public class SecondBezierView extends View {
    //起点
    private float mStartPointX;
    private float mStartPointY;
    //终点
    private float mEndPointX;
    private float mEndPointY;
    //控制点
    private float mFlagPointX;
    private float mFlagPointY;

    private Path mPath;
    private Paint mPaintBezier;
    private Paint mPaintFlag;
    private Paint mPaintFlagText;

    public SecondBezierView(Context context) {
        super(context);
    }

    public SecondBezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setStrokeWidth(8);
        mPaintBezier.setStyle(Paint.Style.STROKE);

        mPaintFlag = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintFlag.setStrokeWidth(3);
        mPaintFlag.setStyle(Paint.Style.STROKE);

        mPaintFlagText = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintFlagText.setStyle(Paint.Style.STROKE);
        mPaintFlagText.setTextSize(20);
    }

    public SecondBezierView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //初始时确定起点、终点和控制点的坐标
        mStartPointX = w / 4;
        mStartPointY = h / 2 - 200;

        mEndPointX = w * 3 / 4;
        mEndPointY = h / 2 - 200;

        mFlagPointX = w / 2;
        mFlagPointY = h / 2 - 300;

        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath.moveTo(mStartPointX, mStartPointY);
        mPath.quadTo(mFlagPointX, mFlagPointY, mEndPointX, mEndPointY);

        canvas.drawPoint(mStartPointX, mStartPointY, mPaintFlag);
        canvas.drawText("起点", mStartPointX, mStartPointY, mPaintFlagText);
        canvas.drawPoint(mEndPointX, mEndPointY, mPaintFlag);
        canvas.drawText("终点", mEndPointX, mEndPointY, mPaintFlagText);
        canvas.drawPoint(mFlagPointX, mFlagPointY, mPaintFlag);
        canvas.drawText("控制点", mFlagPointX, mFlagPointY, mPaintFlagText);
        canvas.drawLine(mStartPointX, mStartPointY, mFlagPointX, mFlagPointY, mPaintFlag);
        canvas.drawLine(mEndPointX, mEndPointY, mFlagPointX, mFlagPointY, mPaintFlag);

        canvas.drawPath(mPath, mPaintBezier);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mFlagPointX = event.getX();
                mFlagPointY = event.getY();
                invalidate();
                break;
        }
        return true;
    }
}

效果图如下:
在这里插入图片描述

2、波浪效果

波浪效果的实现关键就是借助波浪的周期性规律和ValueAnimator位移偏量0到波长间重复的变化,通过不断的位移变化,达到波浪流动的效果

(1)实现一个完整的波浪

一个完整的波浪需要两个贝塞尔曲线,这里设定半波长为400,连续调用两次quadTo方法:

mPath.moveTo(0, mStartPointY);
mPath.quadTo(200, mStartPointY-300, 400, mStartPointY);
mPath.quadTo(600, mStartPointY+300, 800, mStartPointY);

在这里插入图片描述
接下来的任务是让波浪填满屏幕宽度,且流动起来。因此需要弄清两个问题:屏幕宽度可以容纳几个周期波浪,填满屏幕后每次波浪位移量多少?通过改变波浪的横坐标来达到让波浪流动的效果。

  1. 前者获取屏幕宽度计算即可。
  2. 后者只需增加二阶贝塞尔曲线的起点、终点、控制点横坐标位移量即可,使其呈现出流动的动画效果。但需要注意当波浪向右侧流动时,屏幕左侧之外应当也有波形,使得波形准备向右侧移动一个弦长的距离时,屏幕左侧之外也有一个弦长的波形准备移动进来,不然波浪之间则会断开

(2)将波浪填满屏幕

  1. 首先在自定义View的构造方法中定义一个完整的波浪长度为800,即包括上圆拱和下圆拱部分。
  2. 在初始定义View大小的void onSizeChanged(int w, int h, int oldw, int oldh)方法中获取屏幕宽度计算填满屏幕需要几个完整波长。注意此处在计算时不可直接简单为mScreenWidth / mWaveLength,首先考虑到除法操作后结果为Double类型,再赋值给int类型count,因此避免舍位带来的误差,需要在除法过后加上0.5,而且之前一直在强调,屏幕左侧之外也应当有一个弦长的波形准备移动进来,因此再加上1,最后公式为mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
  3. 因此绘制时第一个波形的起点是屏幕外侧的 -波形长坐标,调用mPath。move确定好起点后,循环mWaveCount数量绘制波形,每次循环绘制一个波形,即之前讲过的调用两次quadTo方法。
    在这里插入图片描述

(3)让波浪动起来

这里我们利用ValueAnimator让波浪动起来,并设置一个插值器来控制坐标点的偏移。完整代码如下:

public class WaveBezierView extends View implements View.OnClickListener {
    private Path mPath;

    private Paint mPaintBezier;

    private int mWaveLength;
    private int mScreenHeight;
    private int mScreenWidth;
    private int mCenterY;
    private int mWaveCount;

    private ValueAnimator mValueAnimator;
    //波浪流动X轴偏移量
    private int mOffsetX;
    //波浪升起Y轴偏移量
    private int mOffsetY;
    private int count = 0;

    public WaveBezierView(Context context) {
        super(context);
    }

    public WaveBezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setColor(Color.LTGRAY);
        mPaintBezier.setStrokeWidth(8);
        mPaintBezier.setStyle(Paint.Style.FILL_AND_STROKE);

        mWaveLength = 800;
    }

    public WaveBezierView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPath = new Path();
        setOnClickListener(this);

        mScreenHeight = h;
        mScreenWidth = w;
        mCenterY = h / 2;//设定波浪在屏幕中央处显示

        //此处多加1,是为了预先加载屏幕外的一个波浪,持续报廊移动时的连续性
        mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        //Y坐标每次绘制时减去偏移量,即波浪升高
        mPath.moveTo(-mWaveLength + mOffsetX, mCenterY);
        //每次循环绘制两个二阶贝塞尔曲线形成一个完整波形(含有一个上拱圆,一个下拱圆)
        for (int i = 0; i < mWaveCount; i++) {
            //此处的60是指波浪起伏的偏移量,自定义为60
           /*
            mPath.quadTo(-mWaveLength * 3 / 4 + i * mWaveLength + mOffsetX, mCenterY + 60, -mWaveLength / 2 + i * mWaveLength + mOffset, mCenterY);
            mPath.quadTo(-mWaveLength / 4 + i * mWaveLength + mOffsetX, mCenterY - 60, i * mWaveLength + mOffset, mCenterY);
            */
            //第二种写法:相对位移
            mPath.rQuadTo(mWaveLength / 4, -60, mWaveLength / 2, 0);
            mPath.rQuadTo(mWaveLength / 4, +60, mWaveLength / 2, 0);

        }
        mPath.lineTo(mScreenWidth, mScreenHeight);
        mPath.lineTo(0, mScreenHeight);
        mPath.close();
        canvas.drawPath(mPath, mPaintBezier);
    }

    @Override
    public void onClick(View view) {
        //设置动画运动距离
        mValueAnimator = ValueAnimator.ofInt(0, mWaveLength);
        mValueAnimator.setDuration(1000);
        //设置播放数量无限循环
        mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
//        mValueAnimator.setRepeatCount(1);
        //设置线性运动的插值器
        mValueAnimator.setInterpolator(new LinearInterpolator());
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                //获取偏移量,绘制波浪曲线的X横坐标加上此偏移量,产生移动效果
                mOffsetX = (int) valueAnimator.getAnimatedValue();
                count++;

                invalidate();
            }
        });
        mValueAnimator.start();
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值