自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果

学完了上面几章比较基础的,终于到了比较有意思的环节了。上篇draw Text都没全部学完,太枯燥了,现在继续往下学。太菜了,必须逼自己好好学习。
有四个函数与贝赛尔曲线有关:

//二阶贝赛尔
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
//三阶贝赛尔
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)

1、贝赛尔曲线来源
在数学的数值分析领域中,贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞。还是要稍微的了解一下的。
骚味看了下解释,挺复杂得,先看懂了。再做例子,再上例子把
对数学得贝塞尔曲线是一窍不通,看一下Android中贝塞尔曲线的使用吧
二、Android中贝赛尔曲线之quadTo
1、quadTo使用原理
这部分我们先来看看quadTo函数的用法,其定义如下:

public void quadTo(float x1, float y1, float x2, float y2)

参数中(x1,y1)是控制点坐标,(x2,y2)是终点坐标
大家可能会有一个疑问:有控制点和终点坐标,那起始点是多少呢?
整条线的起始点是通过Path.moveTo(x,y)来指定的,而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;大家可能还是有点迷糊,下面我们就举个例子来看看
我们利用quadTo()来画下面的这条波浪线:
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第1张图片
解释就看这边大佬的解释吧
https://blog.csdn.net/harvic880925/article/details/50995587
代码:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.GREEN);

        Path path = new Path();
        path.moveTo(100, 300);
        path.quadTo(200, 200, 300, 300);
        path.quadTo(400, 400, 500, 300);
        canvas.drawPath(path, paint);
    }

这里最重要的就是在onDraw(Canvas canvas)中创建Path的过程
path.moveTo(100,300)来指定的,之后后一个path.quadTo的起始点是以前一个path.quadTo的终点为起始点的
效果图
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第2张图片
实例联系—>手势轨迹
效果图:
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第3张图片
代码:

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //起始点就是用户点击的位置
                mPath.moveTo(event.getX(), event.getY());
                return true;
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(event.getX(), event.getY());
                postInvalidate();//重绘
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(mPath, paint);
    }

主要在onTouchEvent方法里面监听到用户手指按下,也就是 MotionEvent.ACTION_DOWN,成为第一个开始的点,也就是moveTo
在用户手指移动时候调用lineTo持续绘画,并不断刷新重绘 postInvalidate(),最后,在onDraw里面画出来即可。
然后这边有两个需要注意的:
第一:有关在case MotionEvent.ACTION_DOWN时return true的问题:return true表示当前控件已经消费了下按动作,之后的ACTION_MOVE、ACTION_UP动作也会继续传递到当前控件中;如果我们在case MotionEvent.ACTION_DOWN时return false,那么后序的ACTION_MOVE、ACTION_UP动作就不会再传到这个控件来了。有关动作拦截的知识,后续会在这个系列中单独来讲,大家先期待下吧。
第二:这里重绘控件使用的是postInvalidate();而我们以前也有用Invalidate()函数的。这两个函数的作用都是用来重绘控件的,但区别是Invalidate()一定要在UI线程执行,如果不是在UI线程就会报错。而postInvalidate()则没有那么多讲究,它可以在任何线程中执行,而不必一定要是主线程。其实在postInvalidate()就是利用handler给主线程发送刷新界面的消息来实现的,所以它是可以在任何线程中执行,而不会出错。而正是因为它是通过发消息来实现的,所以它的界面刷新可能没有直接调Invalidate()刷的那么快。
所以在我们确定当前线程是主线程的情况下,还是以invalide()函数为主。当我们不确定当前要刷新页面的位置所处的线程是不是主线程的时候,还是用postInvalidate为好;

2、实现方式二(优化):使用Path.quadTo()函数实现过渡

说实话这种方式看的还是有点蒙的。上面那种看的比较好理解,但是这种效果更好。
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第4张图片
再看一下分析图:
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第5张图片
具体分析还是看上面链接启舰大神的分析。
这边看一下代码,根据我的理解
先上一下效果图:
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第6张图片
其实我看不出太大变化,再看一下代码。

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //获取到用户点击的位置
                //起始点就是用户点击的位置
                mPath.moveTo(event.getX(), event.getY());
                //这边是用来保存上一个手指的点
                mpreX = event.getX();
                mpreY = event.getY();
                return true;
            case MotionEvent.ACTION_MOVE:
                //这边持续绘画的点就是点击距离的一半
                float endX = (mpreX + event.getX()) / 2;
                float endY = (mpreY + event.getY()) / 2;
                //这边的方法变贝塞尔曲线方式
                mPath.quadTo(mpreX, mpreY, endX, endY);
                mpreX = event.getX();
                mpreY = event.getY();
                postInvalidate();//重绘
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

四、Path.rQuadTo()

函数声明:

public void rQuadTo(float dx1, float dy1, float dx2, float dy2)

参数说明:
dx1:控制点X坐标,表示相对上一个终点X坐标的位移坐标,可为负值,正值表示相加,负值表示相减;
dy1:控制点Y坐标,相对上一个终点Y坐标的位移坐标。同样可为负值,正值表示相加,负值表示相减;
dx2:终点X坐标,同样是一个相对坐标,相对上一个终点X坐标的位移值,可为负值,正值表示相加,负值表示相减;
dy2:终点Y坐标,同样是一个相对,相对上一个终点Y坐标的位移值。可为负值,正值表示相加,负值表示相减;

2.使用rQuadTo实现波浪线
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第7张图片
代码:

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
        mPath.moveTo(100, 300);
        mPath.rQuadTo(100,-100,200,0);
        mPath.rQuadTo(100,100,200,0);
        canvas.drawPath(mPath, paint);
    }

也就是将

path.moveTo(100,300);
path.quadTo(200,200,300,300);
path.quadTo(400,400,500,300);

转换成

path.moveTo(100,300);
path.rQuadTo(100,-100,200,0);
path.rQuadTo(100,100,200,0);

效果图:
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第8张图片

五、实现波浪效果

1、实现全屏波纹
上面我们已经能够实现一个波形,只要我们再多实现几个波形,就可以覆盖整个屏幕了。
上代码:

package com.example.viewstudaydemo.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;


public class WaveView extends View {

    private Path path;
    private Paint paint;
    private int mItemWaveLenght = 400;

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

    public WaveView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        path = new Path();
        paint = new Paint();
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setColor(Color.GREEN);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //先初始化
        path.reset();
        int originY = 300;
        //起点,在屏幕外面
        //mPath的起始位置向左移一个波长
        path.moveTo(-mItemWaveLenght, originY);
        int mHalfWaveLen = mItemWaveLenght / 2;
        for (int i = -mItemWaveLenght; i < getWidth() + mItemWaveLenght; i += mItemWaveLenght) {
            path.rQuadTo(mHalfWaveLen / 2, -100, mHalfWaveLen, 0);
            path.rQuadTo(mHalfWaveLen / 2, 100, mHalfWaveLen, 0);
        }

        path.lineTo(getWidth(), getHeight());
        path.lineTo(0, getHeight());
        path.close();
        canvas.drawPath(path, paint);
    }
}

也是主导在onDraw里面画的,画出一个填充屏幕的波形,然后把下面内容填充
,效果图:
自定义View学习⑥Path之贝赛尔曲线和手势轨迹、水波纹效果_第9张图片

2、实现移动动画
让波纹动起来其实挺简单,利用调用在path.moveTo的时候,将起始点向右移动即可实现移动,而且只要我们移动一个波长的长度,波纹就会重合,就可以实现无限循环了。
为此我们定义一个动画:
全部代码:

package com.example.viewstudaydemo.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;

import androidx.annotation.Nullable;


public class WaveView extends View {

    private Path path;
    private Paint paint;
    private int mItemWaveLenght = 400;
    private int dx;

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

    public WaveView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        path = new Path();
        paint = new Paint();
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setColor(Color.GREEN);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //先初始化
        path.reset();
        int originY = 300;
        //起点,在屏幕外面
        //mPath的起始位置向左移一个波长
        int mHalfWaveLen = mItemWaveLenght / 2;
        path.moveTo(-mItemWaveLenght + dx, originY);
        for (int i = -mItemWaveLenght; i < getWidth() + mItemWaveLenght; i += mItemWaveLenght) {
            path.rQuadTo(mHalfWaveLen / 2, -100, mHalfWaveLen, 0);
            path.rQuadTo(mHalfWaveLen / 2, 100, mHalfWaveLen, 0);
        }

        path.lineTo(getWidth(), getHeight());
        path.lineTo(0, getHeight());
        path.close();
        canvas.drawPath(path, paint);
    }

    public void startAnim() {
        final ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mItemWaveLenght);
        //持续时间,2000毫秒
        valueAnimator.setDuration(2000);
        //循环次数,无限循环
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        //匀速
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                dx = (int) valueAnimator.getAnimatedValue();
                //重绘
                postInvalidate();
            }
        });
        valueAnimator.start();
    }
}

学习文章:https://blog.csdn.net/harvic880925/article/details/50995587

你可能感兴趣的:(Android)