Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

栏目: IOS · Android · 发布时间: 5年前

内容简介:大家好,我是深红骑士,爱开玩笑,技术一渣渣,热爱钻研,这篇文章是今年的最后一篇了,首先祝大家在新的一年里心想事成,诸事顺利。今天来学习贝塞尔曲线,之前一直想学,可惜没时间。什么是贝塞尔曲线呢?一开始我也是不懂的,当查了很多资料,现在还是不够了解,其推导公式还是不能深入了解。对发布这曲线的法国工程师上面这个动画是不是很炫,它就是用贝塞尔曲线来实现的。贝塞尔曲线是用一系列点来控制曲线状态的,我将这一系列点分为三个点:

大家好,我是深红骑士,爱开玩笑,技术一渣渣,热爱钻研,这篇文章是今年的最后一篇了,首先祝大家在新的一年里心想事成,诸事顺利。今天来学习贝塞尔曲线,之前一直想学,可惜没时间。什么是贝塞尔曲线呢?一开始我也是不懂的,当查了很多资料,现在还是不够了解,其推导公式还是不能深入了解。对发布这曲线的法国工程师 皮埃尔·贝塞尔 由衷敬佩,贝塞尔曲线,又称贝兹曲线或者贝济埃曲线,是应用于 二维图形 应用程序的数学曲线.1962年, 皮埃尔·贝塞尔 运用贝塞尔曲线为汽车的主体进行设计,贝塞尔曲线最初由Paul de Casteljau于1959年运用 de Casteljau 算法开发,以稳定的述职方法求出贝塞尔曲线。其实贝塞尔曲线就在我们日常生活中,如一些成熟的位图软件中:PhotoSHop,Flash5等。在前端开发中,贝塞尔曲线也是无处不在:前端2D或者3D图形图标库都会使用贝塞尔曲线;它可以用来绘制曲线,在svg和canvas中,原生提供的曲线绘制都是用贝塞尔曲线实现的;在css的 transition-timing-function 属性,可以使用贝塞尔曲线来描述过渡的缓动计算。

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

上面这个动画是不是很炫,它就是用贝塞尔曲线来实现的。

贝塞尔曲线原理

贝塞尔曲线是用一系列点来控制曲线状态的,我将这一系列点分为三个点: 起点终点控制点 。通过改变这些点,贝塞尔曲线就会发生变化。

  • 起点:确定曲线的起点
  • 终点:确定曲线的终点
  • 控制点:确定曲线的控制点

一阶曲线原理

一阶曲线就是一条直线,只有两个点,就是 起点终点 ,也就是最终效果就是一条线段。还是直接上图比较直观:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

一阶公式如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

那么上面的公式是怎么来的呢?为了方便,我就在纸上写了,字有点丑,见谅了:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

二阶曲线原理

二阶曲线由两个数据点(起始点和终点),一个控制点来描述曲线状态,如下图,下面A点是起始点,C是终点,B是控制点。

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

红线AC是怎么生成的呢?继续上图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

简单来看连接AB,BC两条线段,在AB,BC分别取D,E两点,连接DE。如下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

D在AB线段上从A往B做一阶曲线运动,E在BC线段上从B往C做一阶曲线运动,而F在DE上做一阶曲线运动,那么F这点就是贝塞尔曲线上的一个点,动态图如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
再简单理解就是:二阶贝塞尔曲线就是 起点终点

不断变化的一阶贝塞尔曲线。二阶公式如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

那么上面这个公司怎么推导出来的呢?同样为了方便,我就在纸上写了:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

三阶曲线原理

三阶曲线其实就是由两个数据点(起始点和终点),两个控制点来描述曲线的状态,如下图,下面A是起始点,D是终点,B和C是控制点。

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

动态图如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

可以这么理解,两个数据点和控制点不断变化的二阶贝塞尔曲线,即拆分为p0p1p2和p1p2p3两个二阶贝塞尔曲线。三阶公式如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

那么上面这个公式是怎么推导出来的呢?直接上图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

四阶,五阶的效果图和推导公式就不上了,原理是一样的。通过上面一阶,二阶,三阶的推导可以发现这样一个规律:没N阶贝塞尔曲线都可以拆分为两个N-1阶,和高数中二项式展开一样,就是阶数越高,控制点之间就会越近,绘制的曲线就会更加丝滑。通用公式如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

把贝塞尔曲线原理弄懂了,下面就可以用来做实际性的东西了。

Android中的贝塞尔曲线

在Android中,Path类中有四个方法与贝塞尔曲线相关的,也就是已经封装了关于贝塞尔曲线的函数,开发者直接调用即可:

//二阶贝赛尔
    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);
复制代码

上面的四个函数中,quadTo、rQuadTo是二阶贝塞尔曲线,cubicTo、rCubicTo是三阶贝塞尔曲线。因为三阶贝塞尔曲线使用方法和二阶贝塞尔曲线相似,用处也很少,就不细说了。下面就针对二阶的贝塞尔曲线quadTo、rQuadTo为详细说明。

quadTo原理

先看看quadTo函数的定义:

/**
     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
     * this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end point on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }
复制代码

看上面的注释可以知道:(x1,y1)是控制点,(x2,y2)是终点坐标,怎么没有起点的坐标呢?作为Android开发者都知道,一条线段的起始点都是通过Path.move(x,y)来指定的。如果连续调用quadTo函数,那么前一个quadTo的终点就是下一个quadTo函数的起始点,如果初始化没有调用Path.moveTo(x,y)来指定起始点,那么控件视图就会以 左上角(0,0)为起始点 ,还是直接上例子描述。 下面实现绘制下面以下效果图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

下面先通过PhotoShop来模拟画出上面这条轨迹的辅助控制点的位置:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
下面通过草图分析确定起始点,终点,控制点的位置, 注意

,下面的分析图位置不是很准备,只是为了确定控制点的位置。

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

先看p0-p1这条路径,是以p0为起始点,p2为终点,p1为控制点。起始的坐标设置为(200,400),终点的坐标设置(400,400),控制点是在p0,p1的上方,因此纵坐标y的值比两点都要小,横坐标的位置是在p0和p2的中间。那么p1坐标设定为(300,300);同理,在p2-p4的这条二阶贝塞尔曲线上,控制点p3的坐标位置应该是(500,500),因为p0-p2,p2-p4这两条贝塞尔曲线是对称的。

示例代码

public class PathView extends View {


    //画笔
    private Paint paint;
    //路径
    private Path path;

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

    public PathView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    //重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //线条宽度
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        //设置起始点的位置为(200,400)
        path.moveTo(200,400);
        //线条p0-p2控制点(300,300) 终点位置(400,400)
        path.quadTo(300,300,400,400);
        //线条p2-p4控制点(500,500) 终点位置(600,400)
        path.quadTo(500,500,600,400);
        canvas.drawPath(path, paint);

    }


    private void init() {
        paint = new Paint();
        path = new Path();
    }
}
复制代码

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">



    <Button
        android:id="@+id/btn_reset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        android:text="清空路径"
        />

    <com.example.okhttpdemo.PathView
        android:id="@+id/path_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/btn_reset"
        android:background="#000000"/>


</android.support.constraint.ConstraintLayout>
复制代码

效果图如下图所示:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
下面把 path.moveTo(200,400);

注释,再看看效果:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

通过上面的简单例子,可以得出以下两点:

  • 当连续调用quadTo函数时,前一个quadTo函数的终点就是调用下一个quadTo函数的起始点。
  • 贝塞尔曲线的起点是通过Path.moveTo(x,y)来指定的,如果一开始没有调用Path.move(x,y),则会取控件的左上角(0,0)作为起点。

Path.lineTo和Path.quadTo的区别

下面来看看Path.lineTo和Path.quadTo的区别,Path.lineTo是连接直线,是连接上一个点到当前点的之间的直线,下面来实现绘制手指在屏幕上所走的路径,也不难就在上面的基础上增加 onTouchEvent 方法即可,代码如下:

public class PathView extends View {


    //画笔
    private Paint paint;
    //路径
    private Path path;

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

    public PathView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    //重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //线条宽度
      //  paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        canvas.drawPath(path, paint);

    }


    private void init() {
        paint = new Paint();
        path = new Path();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("ssd","触发按下");
                path.moveTo(event.getX(), event.getY());
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                Log.d("ssd","触发移动");
                path.lineTo(event.getX(), event.getY());
                invalidate();
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }


    public void reset() {
        path.reset();
        invalidate();
    }



}
复制代码

直接上效果图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
当用户点击屏幕时,首先触发的是 MotionEvent.DOWN 这个条件,然后调用 path.move(event.getX(),event.getY()) ,当用户移动手指时,就用 path.lineTo(event.getX,event.getY()) 将各个点连接起来,然后调用 invalidate 重新绘制。这里简单说一下在 MotionEvent.ACTION_DOWN 为什么要返回 return truereturn true 表示当前的控件已经消费了按下事件,剩下的 ACTION_UPACTION_MOVE 都会被执行;如果在 case MotionEvent.ACTION_DOWN 下返回 return false ,后续的 MOTION_MOVEMMOTION_UP 都不会被接收到,因为没有消费 ACTION_DOWN ,系统就会认为 ACTION_DOWN 没有发生过,所以 ACTION_MOVEACTION_UP

就不能捕获,下面把图放大仔细看:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

把C放大后,很明显看出,这个C字不是很平滑,像是很多一折一折的线段构成,出现这样的原因也很简单分析出来,因为这个C字是由各个不同点之间连线构成的,之间就没有平滑过渡,如果横纵坐标变化剧烈时,更加突出有折痕效果。如果要解决这问题,这时候二阶曲线的作用体现出来了。

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
上图中,有三个黑点连成两条直线,从两个线段可以看出,如果使用Path.lineTo的时候,是直接把触摸点p0,p1,p2连接起来,那么现在要实现这个三个点之间的流畅过渡,我想到的就是把这 两条线的中点分别作为起点和终点 ,把连接这两条线段的点(p1)作为 控制点

,那这样就能解决上面折痕的问题,直接上效果图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

但是上面会有这样一个问题:当你绘制二阶曲线的时候,结束的时候,最开始那段线段的前半部分也就是p0-p3,最后那段线段的后半部分也就是p4-p2不会绘制出来。其实这两端距离可以忽略不计,因为手指滑动的时候,所产生的点与点之间的距离是很小的,因此p0-p3,p4-p2的距离可以忽略不算了。每个图形中,肯定有很多点共同连接线段,而现在就是将两个线段的中间做为二阶曲线的起点和终点,把线段与线段之间的转折点做为控制点,这样来组成平滑的连线。理论上应该可以,那就下面之间敲代码:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("ssd","触发按下");
                path.moveTo(event.getX(), event.getY());
                //保存这个点的坐标
                mBeforeX = event.getX();
                mBeforeY = event.getY();
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                Log.d("ssd","触发移动");
                //移动的时候绘制二阶曲线
                //终点是线段的中点
                endX = (mBeforeX + event.getX()) / 2;
                endY = (mBeforeY + event.getY()) / 2;
                //绘制二阶曲线
                path.quadTo(mBeforeX,mBeforeY,endX,endY);
                //然后更新前一点的坐标
                mBeforeX = event.getX();
                mBeforeY = event.getY();
                invalidate();
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }

复制代码

这里简单说明一下,在 ACTION_DOWN 的时候,先调用 path.moveTo(event.getX(), event.getY()); 设置曲线的初始位置就是手指触屏的位置,上面也解释了如果不调用 moveTo(event.getX(), event.getY()) 的话,那么绘制点就会从控件的(0,0)开始。用 mBeforeXmBeforeY 记录手指移动的前一个横纵坐标,而这个点是做 控制点 ,最后返回 return true 为了让 ACTION_MOVEACTION_UP 向本控件传递。下面说说在 ACTION_MOVE 方法的逻辑处理,首先是确定 结束点 ,上面也说了结束点是线段的中间位置,所以用了两条公式来 endX = (mBeforeX + event.getX()) / 2;endY = (mBeforeY + event.getY()) / 2; 求这个中间位置的横纵坐标,而控制点就是上个手指触摸屏幕的位置,后面就是更新前一个手指坐标。这里注意一下,上面也说了当连续调用 quardTo 的时候,第一个起始点是 Path.moveTo(x,y) 来设置的,其他部分,前面调用 quadTo 的终点是下一个 quard 的起点,这里所说的起始点就是上一个线段的中间点。上面的逻辑用一句话表示:把各个线段的中间点作为起始点和终点,把前一个手指位置作为控制点,最终效果如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
可以看到通过 quadT

实现的曲线会更顺滑。

Path.rQuadTo原理

直接看这个函数的说明:

/**
     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
     * this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end point on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }
复制代码
  • x1:控制点的X坐标,表示相对于上一个终点X坐标的位移值,可以为负值,正值表示相加,负值表示相减
  • x2:控制点的Y坐标,表示相对于上一个终点Y坐标的位移值,可以为负值,正值表示相加,负值表示相减
  • x2:终点的X坐标,表示相对于上一个终点X坐标的位移值,可以为负值,正值表示相加,负值表示相减
  • y2:终点的Y坐标,表示相对一上一个终点Y坐标的位移值,可以为负值,正值表示相加,负值表示相减 这么说可能不理解,下面还是直接举例子: 如果上一个终点坐标是(100,200),如果这时候调用rQuardTo(100,-100,200,200),得到的控制点坐标是(100 + 100,200 - 100 )就是(200,100),得到的终点坐标是(100 + 200,200 + 200)就是(300,400),下面两段是相等的:
path.moveTo(100,200);
path.quadTo(200,100,300,400);
复制代码
path.moveTo(100,200);
path.rQuadTo(100,-100,200,200);
复制代码

在上面中,用 quadTo 实现了一个波浪线,下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
下面是上面用 quadTo

实现的代码:

//重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //线条宽度
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        //设置起始点的位置为(200,400)
        path.moveTo(200,400);
        //线条p0-p2控制点(300,300) 终点位置(400,400)
        path.quadTo(300,300,400,400);
        //线条p2-p4控制点(500,500) 终点位置(600,400)
        path.quadTo(500,500,600,400);
        canvas.drawPath(path, paint);

    }
复制代码

下面就用 rQuadTo 来实现这个波浪线,先上分析图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

代码如下:

//重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //线条宽度
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        //设置起始点的位置为(200,400)
        path.moveTo(200,400);
        //线条p0-p2控制点(300,300) 终点坐标位置(400,400)
        path.rQuadTo(100,-100,200,0);
        //线条p2-p4控制点(500,500) 终点坐标位置(600,400)
        path.rQuadTo(100,100,200,0);
        canvas.drawPath(path, paint);

    }
复制代码

第一行:path.rQuadTo(100,-100,200,0);这个一行代码是基于(200,400)这个点来计算曲线p0-p2的控制点和终点坐标。

  • 控制点X坐标 = 上一个终点的X坐标 + 控制点X位移值 = 200 + 100 = 300;
  • 控制点Y坐标 = 上一个终点的Y坐标 + 控制点Y位移值 = 400 - 100 = 300;
  • 终点X坐标 = 上一个终点的X坐标 + 终点X的位移值 = 200 + 200 = 400;
  • 终点Y坐标 = 上一个终点的Y坐标 + 终点Y的位移值 = 400 + 0 = 400; 这句和path.quadTo(300,300,400,400)是等价的。

那么第一条曲线就容易绘制出来了,并且第一条曲线的终点也知道了是(400,400),那么第二句path.rQuadTo(100,100,200,0)是基于这个终点(400,400)来计算第二条曲线的控制点和终点。

  • 控制点X坐标 = 上一个终点的X坐标 + 控制点X位移值 = 400 + 100 = 500;
  • 控制点Y坐标 = 上一个终点的Y坐标 + 控制点Y位移值 = 400 + 100 = 500
  • 终点X坐标 = 上一个终点的X坐标 + 终点X的位移值 = 400 + 200 = 600;
  • 终点Y坐标 = 上一个终点的Y坐标 + 终点Y的位移值 = 400 + 0 = 400;

其实这句 path.rQuadTo(100,100,200,0); 是和 path.quadTo(500,500,600,400); 相等的,实际运行的效果图也和用 quadTo 方法绘制的一样,通过这个例子,可以知道 quadTo 这个方法的参数都是实际结果的坐标,而 rQuadTo 这个方法的参数是以上一个终点位置为基准来做位移的。

实现封闭波浪

下面要实现以下效果:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

实现静态封闭波浪

对应代码如下:

//重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.reset();
        //设置填充绘制
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        //线条宽度
        //paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        int control = waveLength / 2;
        //首先确定初始状态的起点(-400,1200)
        path.moveTo(-waveLength,origY);
        //因为这个整个波浪的的宽度是View宽度加上左右各一个波长
        for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
            path.rQuadTo(control / 2,-70,control,0);
            path.rQuadTo(control / 2,70,control,0);
        }
        path.lineTo(getWidth(),getHeight());
        path.lineTo(0,getHeight());
        path.close();
        canvas.drawPath(path, paint);

    }
复制代码

下面一行一行分析:

//首先确定初始状态的起点(-400,1200)
        path.moveTo(-waveLength,origY);
复制代码

首先将 Path 起始位置向左移一个波长,为了就是后面实现的位移动画,然后利用循环来画出屏幕所容下的所有波浪:

for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
            path.rQuadTo(control / 2,-70,control,0);
            path.rQuadTo(control / 2,70,control,0);
        }
复制代码

这里我简单说一下, path.rQuadTo(control / 2,-70,control,0); 循环里的第一行画的是一个波长的前半部分,下面把数值放进去就很容易理解了,因为 waveLength 是400,所以 control = waveLength / 2 就是200,而 path.rQuadTo(control / 2,-70,control,0) 就是 path.rQuadTo(100,-70,200,0) ,而 path.rQuadTo(control / 2,70,control,0) 就是 path.rQuadTo(100,70,200,0) ,上面说过 rQuadTo 的用法了,就不再叙述,下面直接上分析图,下面只是 分析最左边的第一个波浪起始点,控制点的坐标,其余波浪只是通过循环绘制 ,就不分析了:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
因为需要连续调用两次 rQuadTo 方法才能绘制出一个完整的波浪,所以上面分析需要确定五个点的位置。这里注意,上面图左右有一条线段连接底部,形成封闭图形,因为要填充内部,所以要封闭绘制 paint.setStyle(Paint.Style.FILL_AND_STROKE); 。当波浪绘制完成时, path 点会在A点,然后用 path.lineTo(getWidth(),getHeight()); 连接A,B点,再调用 path.lineTo(0,getHeight()); 连接B,C点,最后调用 path.close();

连接初始点就是连接C和起始点,这样满横屏的波浪就绘制完成了。

实现位移动画的波浪

下面实现左右上下位移动画,这就会有一点点进度条的感觉,我的做法很简单,因为一开始在View的左边多画了一个波浪,也就是说,将起始点向右边移动,并且要移动一个波浪的长度就可以让波纹重合,然后不断循环即可,简单来讲就是,动画移动的距离是一个波浪的长度,当移动到最大的距离时设置不断循环,就会重新绘制波浪的初始状态。

/**
     * 动画位移方法
     */
    public void startAnim(){
        //创建动画实例
        ValueAnimator moveAnimator = ValueAnimator.ofInt(0,waveLength);
        //动画的时间
        moveAnimator.setDuration(2500);
        //设置动画次数  INFINITE表示无限循环
        moveAnimator.setRepeatCount(ValueAnimator.INFINITE);
        //设置动画插值
        moveAnimator.setInterpolator(new LinearInterpolator());
        //添加监听
        moveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                moveDistance = (int)animation.getAnimatedValue();
                invalidate();
            }
        });
        //启动动画
        moveAnimator.start();
    }
复制代码

动画的位移距离是一个波浪的长度,并将位移的距离保存到 moveDistance 中,然后开始的时候,在 moveTo 加上这个距离,就可以了,完整代码如下:

//重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //把路线清除 重新绘制 一定要加上 不然是矩形
        path.reset();
        //设置填充绘制
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        //线条宽度
        //paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        int control = waveLength / 2;
        //首先确定初始状态的起点(-400,1200)
        path.moveTo(-waveLength + moveDistance,origY);
        for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
            path.rQuadTo(control / 2,-70,control,0);
            path.rQuadTo(control / 2,70,control,0);
        }
        path.lineTo(getWidth(),getHeight());
        path.lineTo(0,getHeight());
        path.close();
        canvas.drawPath(path, paint);

    }
复制代码

效果如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
上面只是添加横向移动,下面添加垂直的移动,我这边为了方便,垂直移动距离跟横向距离一样,很简单,把初始纵坐标同样减去移动距离,因为是向上移动,所以是要减 path.moveTo(-waveLength + moveDistance,origY - moveDistance);

,最后调用以下代码:

pathView = findViewById(R.id.path_view);
        pathView.startAnim();
复制代码

效果如上上上图。经过上面,自己对贝塞尔曲线由初步的了解,下面就实现波浪形进度条。

实现波浪进度条

学到了上面的基本知识,那下面就实现一个小例子,就是圆形波浪进度条,最终效果在文章最底部,惯例下面就一步一步来实现。

绘制一段波浪

先绘制一段满屏的波浪线,绘制原理就不详细讲了,直接上代码:

/**
 * Describe : 实现圆形波浪进度条
 * Created by Knight on 2019/2/1
 * 点滴之行,看世界
 **/
public class CircleWaveProgressView extends View {

    //绘制波浪画笔
    private Paint wavePaint;
    //绘制波浪Path
    private Path wavePath;
    //波浪的宽度
    private float waveLength;
    //波浪的高度
    private float waveHeight;
    public CircleWaveProgressView(Context context) {
        this(context,null);
    }

    public CircleWaveProgressView(Context context,  @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CircleWaveProgressView(Context context,  @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    /**
     * 初始化一些画笔路径配置
     * @param context
     */
    private void  init(Context context){
        //设置波浪宽度
        waveLength = Density.dip2px(context,25);
        //设置波浪高度
        waveHeight = Density.dip2px(context,15);
        wavePath = new Path();
        wavePaint = new Paint();
        wavePaint.setColor(Color.parseColor("#ff7c9e"));
        //设置抗锯齿
        wavePaint.setAntiAlias(true);
    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        //绘制波浪线
        canvas.drawPath(paintWavePath(),wavePaint);
    }

    /**
     * 绘制波浪线
     *
     * @return
     */
    private Path paintWavePath(){
        //要先清掉路线
        wavePath.reset();
        //起始点移至(0,waveHeight)
        wavePath.moveTo(0,waveHeight);
        for(int i = 0;i < getWidth() ;i += waveLength){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        return wavePath;
    }

}

复制代码

xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
>
    <com.example.progressbar.CircleWaveProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

</android.support.constraint.ConstraintLayout>
复制代码

实际效果如下:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

下面绘制封闭波浪,效果分析图如下:

绘制封闭静态波浪

因为圆形进度框中的波浪是随着进度的增加而不断上升的,所以波浪是填充物,先绘制波浪,然后用 path.lineTopath.close 来连接封闭起来,构成一个填充图形,分析如下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

绘制顺序是p0-p1-p2-p3,代码如下:

public class CircleWaveProgressView extends View {

    //绘制波浪画笔
    private Paint wavePaint;
    //绘制波浪Path
    private Path wavePath;
    //波浪的宽度
    private float waveLength;
    //波浪的高度
    private float waveHeight;
    //波浪组的数量 一个波浪是一低一高
    private int waveNumber;
    //自定义View的波浪宽高
    private int waveDefaultSize;
    //自定义View的最大宽高 就是比波浪高一点
    private int waveMaxHeight;

    public CircleWaveProgressView(Context context) {
        this(context,null);
    }

    public CircleWaveProgressView(Context context,  @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CircleWaveProgressView(Context context,  @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    /**
     * 初始化一些画笔路径配置
     * @param context
     */
    private void  init(Context context){
        //设置波浪宽度
        waveLength = Density.dip2px(context,25);
        //设置波浪高度
        waveHeight = Density.dip2px(context,15);
        //设置自定义View的宽高
        waveDefaultSize = Density.dip2px(context,250);
        //设置自定义View的最大宽高
        waveMaxHeight = Density.dip2px(context,300);
        //Math.ceil(a)返回求不小于a的最小整数
        // 举个例子:
        // Math.ceil(125.9)=126.0
        // Math.ceil(0.4873)=1.0
        // Math.ceil(-0.65)=-0.0
        //这里是调整波浪数量 就是View中能容下几个波浪 用到ceil就是一定让View完全能被波浪占满 为循环绘制做准备 分母越小就约精准
        waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveDefaultSize / waveLength / 2)));
        wavePath = new Path();
        wavePaint = new Paint();
        //设置颜色
        wavePaint.setColor(Color.parseColor("#ff7c9e"));
        //设置抗锯齿
        wavePaint.setAntiAlias(true);

    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        //绘制波浪线
        canvas.drawPath(paintWavePath(),wavePaint);
        Log.d("ssd",getWidth()+"");
    }

    /**
     * 绘制波浪线
     *
     * @return
     */
    private Path paintWavePath(){
        //要先清掉路线
        wavePath.reset();
        //起始点移至(0,waveHeight)
        wavePath.moveTo(0,waveMaxHeight - waveDefaultSize);
        //最多能绘制多少个波浪
        //其实也可以用 i < getWidth() ;i+=waveLength来判断 这个没那么完美
        //绘制p0 - p1 绘制波浪线
        for(int i = 0;i < waveNumber ;i ++){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        //连接p1 - p2
        wavePath.lineTo(waveDefaultSize,waveDefaultSize);
        //连接p2 - p3
        wavePath.lineTo(0,waveDefaultSize);
        //连接p3 - p0
        wavePath.lineTo(0,waveMaxHeight - waveDefaultSize);
        //封闭起来填充
        wavePath.close();
        return wavePath;
    }

复制代码

测量自适应View的宽高

在上面中,发现一个问题,就是宽和高都在初始化方法 init 中定死了,一般来讲视图View的宽高都是在 xml 文件中定义或者类文件中定义的,那么就要重写View的 onMeasure 方法:

@Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
         super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        int height = measureSize(waveDefaultSize, heightMeasureSpec);
        int width = measureSize(waveDefaultSize, widthMeasureSpec);
        //获取View的最短边的长度
        int minSize = Math.min(height,width);
        //把View改为正方形
        setMeasuredDimension(minSize,minSize);
        //waveActualSize是实际的宽高
        waveActualSize = minSize;
        //Math.ceil(a)返回求不小于a的最小整数
        // 举个例子:
        // Math.ceil(125.9)=126.0
        // Math.ceil(0.4873)=1.0
        // Math.ceil(-0.65)=-0.0
        //这里是调整波浪数量 就是View中能容下几个波浪 用到ceil就是一定让View完全能被波浪占满 为循环绘制做准备 分母越小就约精准
        waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveActualSize / waveLength / 2)));


    }

    /**
     * 返回指定的值
     * @param defaultSize 默认的值
     * @param measureSpec 模式
     * @return
     */
    private int measureSize(int defaultSize,int measureSpec) {
        int result = defaultSize;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);

        //View.MeasureSpec.EXACTLY:如果是match_parent 或者设置定值就
        //View.MeasureSpec.AT_MOST:wrap_content
        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == View.MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
        return result;
    }
复制代码

上面就很简单了,就是增加了一个View的实际宽高变量 waveActualSize ,让代码扩展性更强和精确到更高。

绘制波浪上升

下面实现波浪高度随着进度变化而变化,当进度增加时,波浪高度增高,当进度减少时,波浪高度减少,其实很简单,也就是p0-p3,p1-p2的高度根据进度变化而变化,并增加动画,代码增加如下:

//当前进度值占总进度值的占比
    private float currentPercent;
    //当前进度值
    private float currentProgress;
    //进度的最大值
    private float maxProgress;
    //动画对象
    private WaveProgressAnimat waveProgressAnimat;
    
     /**
     * 初始化一些画笔路径配置
     * @param context
     */
    private void  init(Context context){
        //......
        //占比一开始设置为0
        currentPercent = 0;
        //进度条进度开始设置为0
        currentProgress = 0;
        //进度条的最大值设置为100
        maxProgress = 100;
        //动画实例化
        waveProgressAnimat = new WaveProgressAnimat();

    }
    
       /**
     * 绘制波浪线
     *
     * @return
     */
    private Path paintWavePath(){
        //要先清掉路线
        wavePath.reset();
        //起始点移至(0,waveHeight) p0 -p1 的高度随着进度的变化而变化
        wavePath.moveTo(0,(1 - currentPercent) * waveActualSize);
        //最多能绘制多少个波浪
        //其实也可以用 i < getWidth() ;i+=waveLength来判断 这个没那么完美
        //绘制p0 - p1 绘制波浪线
        for(int i = 0;i < waveNumber ;i ++){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        //连接p1 - p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        //连接p2 - p3
        wavePath.lineTo(0,waveActualSize);
        //连接p3 - p0 p3-p0d的高度随着进度变化而变化
        wavePath.lineTo(0,(1 - currentPercent) * waveActualSize);
        //封闭起来填充
        wavePath.close();
        return wavePath;
    }
    
      //新建一个动画类
    public class WaveProgressAnimat extends Animation{


        //在绘制动画的过程中会反复的调用applyTransformation函数,
        // 每次调用参数interpolatedTime值都会变化,该参数从0渐 变为1,当该参数为1时表明动画结束
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            //更新占比
            currentPercent = interpolatedTime * currentProgress / maxProgress;
            //重新绘制
            invalidate();

        }
    }

    /**
     * 设置进度条数值
     * @param currentProgress 当前进度
     * @param time 动画持续时间
     */
    public void setProgress(float currentProgress,int time){
         this.currentProgress = currentProgress;
         //从0开始变化
         currentPercent = 0;
         //设置动画时间
         waveProgressAnimat.setDuration(time);
         //当前视图开启动画
         this.startAnimation(waveProgressAnimat);
    }

复制代码

最后在Activity调用一些代码:

//进度为50 时间是2500毫秒
        circleWaveProgressView.setProgress(50,2500);
复制代码

最终效果如下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

绘制波浪左右平移

上面实现了波浪直线上升的动画,下面实现波浪平移的动画,添加左移的效果,这里想到前面也实现了平移的效果,但是下面实现方式和上面有点出入,简单来讲就是移动p0坐标,但是如果移动p0坐标会出现波浪不铺满整个View的情况,这里运用到一种很常见的循环处理办法。在飞机大战的背景滚动图,是两张背景图拼接起来,当飞机从第一个背景图片最底端出发,向上移动了第一个背景图片高度的距离时,将角色重新放回到第一个背景图片的最底端,这样就能实现背景图片循环的效果。也就是一开始绘制两端p0-p1,然后随着进度变化,p0会左移,一开始不在View中的波浪会从右边往左边移动出现,当滑动最大距离时,又重新绘制最开始状态,这样就达到循环了。还是先上分析图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

View的初始状态是蓝色区域,然后经过动画位移慢慢变成红色区域,代码实现如下:

//波浪平移距离
    private float moveDistance = 0;
    
     /**
     * 绘制波浪线
     *
     * @return
     */
    private Path paintWavePath(){
        //要先清掉路线
        wavePath.reset();
        //起始点移至(0,waveHeight) p0 -p1 的高度随着进度的变化而变化
        wavePath.moveTo(-moveDistance,(1 - currentPercent) * waveActualSize);
        //最多能绘制多少个波浪
        //其实也可以用 i < getWidth() ;i+=waveLength来判断 这个没那么完美
        //绘制p0 - p1 绘制波浪线 这里有一段是超出View的,在View右边距的右边 所以是* 2,为了水平位移
        for(int i = 0; i < waveNumber * 2 ; i ++){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        //连接p1 - p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        //连接p2 - p3
        wavePath.lineTo(0,waveActualSize);
        //连接p3 - p0 p3-p0d的高度随着进度变化而变化
        wavePath.lineTo(0,(1 - currentPercent) * waveActualSize);
        //封闭起来填充
        wavePath.close();
        return wavePath;
    }
    
    
     /**
     * 设置进度条数值
     * @param currentProgress 当前进度
     * @param time 动画持续时间
     */
    public void setProgress(final float currentProgress, int time){
         this.currentProgress = currentProgress;
         //从0开始变化
         currentPercent = 0;
         //设置动画时间
         waveProgressAnimat.setDuration(time);
         //设置循环播放
         waveProgressAnimat.setRepeatCount(Animation.INFINITE);
         //让动画匀速播放,避免出现波浪平移停顿的现象
         waveProgressAnimat.setInterpolator(new LinearInterpolator());
         waveProgressAnimat.setAnimationListener(new Animation.AnimationListener() {
             @Override
             public void onAnimationStart(Animation animation) {

             }

             @Override
             public void onAnimationEnd(Animation animation) {

             }

             @Override
             public void onAnimationRepeat(Animation animation) {
                 //波浪到达最高处后平移的速度改变,给动画设置监听即可,当动画结束后以7000毫秒的时间运行,变慢了
                 if(currentPercent == currentProgress /maxProgress){
                     waveProgressAnimat.setDuration(7000);
                 }
             }
         });
         //当前视图开启动画
         this.startAnimation(waveProgressAnimat);
    }
     //新建一个动画类
    public class WaveProgressAnimat extends Animation{


        //在绘制动画的过程中会反复的调用applyTransformation函数,
        // 每次调用参数interpolatedTime值都会变化,该参数从0渐 变为1,当该参数为1时表明动画结束
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            //波浪高度达到最高就不用循环,只需要平移
            if(currentPercent < currentProgress / maxProgress){
                currentPercent = interpolatedTime * currentProgress / maxProgress;
            }
            //左移的距离根据动画进度而改变
            moveDistance = interpolatedTime * waveNumber * waveLength * 2;
            //重新绘制
            invalidate();

        }
    }
    
复制代码

最后的效果如下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

绘制圆形外框背景

这里要用到 PorterDuffXfermode 的知识,其实也不难,先上 PorterDuff.Mode 各种模式的效果图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
这张图看起来很正常,但这张图其实给 开发者造成很大的误区 ,看下面这篇博文并且自己动手实践一下 android PorterDuffXferMode真正的效果测试集合(对比官方demo) 。 下面用到了 PorterDuff.Mode.SRC_IN ,因为先绘制圆形背景,再绘制波浪线,而 PorterDuff.Mode.SRC_IN

模式在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响,看上图就知道了,代码如下:

//圆形背景画笔
    private Paint circlePaint;
    //bitmap
    private Bitmap circleBitmap;
    //bitmap画布
    private Canvas bitmapCanvas;
    
      /**
     * 初始化一些画笔路径配置
     * @param context
     */
    private void  init(Context context){
        //.......
        //绘制圆形背景开始
        wavePaint = new Paint();
        //设置画笔为取交集模式
        wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        //圆形背景初始化
        circlePaint = new Paint();
        //颜色
        circlePaint.setColor(Color.GRAY);
        //设置抗锯齿
        circlePaint.setAntiAlias(true);
        
    }
    
     @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);

        Log.d("ssd",getWidth()+"");
        //这里用到了缓存 根据参数创建新位图
        circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
        //以该bitmap为低创建一块画布
        bitmapCanvas = new Canvas(circleBitmap);
        //绘制圆形 圆心 直径都是很简单得出
        bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2, circlePaint);
        //绘制波浪形
        bitmapCanvas.drawPath(paintWavePath(),wavePaint);
        //裁剪图片
        canvas.drawBitmap(circleBitmap, 0, 0, null);
        //绘制波浪线
      //  canvas.drawPath(paintWavePath(),wavePaint);

    }

复制代码

实际效果如下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条
简单的进度条终于出来了~下面继续完善,到这里你可能发现,颜色,大小什么的都在类中定死了,但实际中有很多属性都要在布局文件中设置的,同一个View在不同场景下状态可能是不一样的,所以为了提高扩展性,在 res\vaules 文件下添加 attrs.xml 文件,给 CircleWaveProgressView

添加自定义属性,如下:

<!--这里的名字要和自定义的View名称一样,不然在xml布局中无法引用-->
    <declare-styleable name="CircleWaveProgressView">
        <!--波浪的颜色-->
        <attr name="wave_color" format="color"></attr>
        <!--圆形背景颜色-->
        <attr name="circlebg_color" format="color"></attr>
        <!--波浪长度-->
        <attr name="wave_length" format="dimension"></attr>
        <!--波浪高度-->
        <attr name="wave_height" format="dimension"></attr>
        <!--当前进度-->
        <attr name="currentProgress" format="float"></attr>
        <!--最大进度-->
        <attr name="maxProgress" format="float"></attr>
    </declare-styleable>

复制代码

在自定义View为属性值赋值:

//波浪颜色
    private int wave_color;
    //圆形背景进度框颜色
    private int circle_bgcolor;
    
    
    public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取attrs文件下配置属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgressView);
        //获取波浪宽度 第二个参数,如果xml设置这个属性,则会取设置的默认值 也就是说xml没有指定wave_length这个属性,就会取Density.dip2px(context,25)
        waveLength = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_length,Density.dip2px(context,25));
        //获取波浪高度
        waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_height,Density.dip2px(context,15));
        //获取波浪颜色
        wave_color = typedArray.getColor(R.styleable.CircleWaveProgressView_wave_color,Color.parseColor("#ff7c9e"));
        //圆形背景颜色
        circle_bgcolor = typedArray.getColor(R.styleable.CircleWaveProgressView_circlebg_color,Color.GRAY);
        //当前进度
        currentProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_currentProgress,50);
        //最大进度
        maxProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_maxProgress,100);
        //记得把TypedArray回收
        //程序在运行时维护了一个 TypedArray的池,程序调用时,会向该池中请求一个实例,用完之后,调用 recycle() 方法来释放该实例,从而使其可被其他模块复用。
        //那为什么要使用这种模式呢?答案也很简单,TypedArray的使用场景之一,就是上述的自定义View,会随着 Activity的每一次Create而Create,
        //因此,需要系统频繁的创建array,对内存和性能是一个不小的开销,如果不使用池模式,每次都让GC来回收,很可能就会造成OutOfMemory。
        //这就是使用池+单例模式的原因,这也就是为什么官方文档一再的强调:使用完之后一定 recycle,recycle,recycle
        typedArray.recycle();
        init(context);
    }
    
    /**
     * 初始化一些画笔路径配置
     * @param context
     */
    private void  init(Context context){
        //设置自定义View的宽高
        waveDefaultSize = Density.dip2px(context,250);
        //设置自定义View的最大宽高
        waveMaxHeight = Density.dip2px(context,300);

        wavePath = new Path();
        wavePaint = new Paint();
        //设置画笔为取交集模式
        wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        //圆形背景初始化
        circlePaint = new Paint();
        //设置圆形背景颜色
        circlePaint.setColor(circle_bgcolor);
        //设置抗锯齿
        circlePaint.setAntiAlias(true);
        //设置波浪颜色
        wavePaint.setColor(wave_color);
        //设置抗锯齿
        wavePaint.setAntiAlias(true);
        //占比一开始设置为0
        currentPercent = 0;
        //进度条进度开始设置为0
        currentProgress = 0;
        //进度条的最大值设置为100
        maxProgress = 100;
        //动画实例化
        waveProgressAnimat = new WaveProgressAnimat();


复制代码

下面就可以在布局文件自定义设置波浪颜色,高度,宽度以及圆形背景颜色:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.android.quard.CircleWaveProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:wave_color="@color/colorPrimaryDark"
        app:circlebg_color="@android:color/black"
        />

</android.support.constraint.ConstraintLayout>
复制代码

效果图就不贴出来了。

绘制文字进度效果

下面要实现文字显示进度,进度条肯定缺不了具体数值的显示,最简单就是直接在 View 中实现绘制文字的操作,这种是很简单的,我之前实现自定义View都是将逻辑放在里面,这样就显得View很臃肿和扩展性不高,因为你想,假如我现在要改变字体位置和样式,那就需要在这个View去改去大动干戈。假如这个View能开放出处理文字接口的话,也就是后面修改文字样式只通过这个接口就可以了,这样就实现了文字和进度条这个View的解耦。

//进度显示 TextView
    private TextView tv_progress;
    //进度条显示值监听接口
    private UpdateTextListener updateTextListener;
    
        //新建一个动画类
    public class WaveProgressAnimat extends Animation{


        //在绘制动画的过程中会反复的调用applyTransformation函数,
        // 每次调用参数interpolatedTime值都会变化,该参数从0渐 变为1,当该参数为1时表明动画结束
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            //波浪高度达到最高就不用循环,只需要平移
            if(currentPercent < currentProgress / maxProgress){
                currentPercent = interpolatedTime * currentProgress / maxProgress;
                //这里直接根据进度值显示
                tv_progress.setText(updateTextListener.updateText(interpolatedTime,currentProgress,maxProgress));
            }
            //左边的距离
            moveDistance = interpolatedTime * waveNumber * waveLength * 2;
            //重新绘制
            invalidate();

        }
    }
    
    //定义数值监听
    public interface UpdateTextListener{
        /**
         * 提供接口 给外部修改数值样式 等
         * @param interpolatedTime 这个值是动画的 从0变成1
         * @param currentProgress 进度条的数值
         * @param maxProgress 进度条的最大数值
         * @return
         */
        String updateText(float interpolatedTime,float currentProgress,float maxProgress);
    }
    //设置监听 
    public void setUpdateTextListener(UpdateTextListener updateTextListener){
        this.updateTextListener = updateTextListener;

    }

    /**
     *
     * 设置显示内容
     * @param tv_progress 内容 数值什么都可以
     *
     */
    public void setTextViewVaule(TextView tv_progress){
        this.tv_progress = tv_progress;

    }
复制代码

然后在 Activity 文件实现 CircleWaveProgressView.UpdateTextListener 接口,进行逻辑处理:

public class MainActivity extends AppCompatActivity {

    private CircleWaveProgressView circleWaveProgressView;
    private TextView tv_value;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //TextView控件
        tv_value = findViewById(R.id.tv_value);
        //进度条控件
        circleWaveProgressView = findViewById(R.id.circle_progress);
        //将TextView设置进度条里
        circleWaveProgressView.setTextViewVaule(tv_value);
        //设置字体数值显示监听
        circleWaveProgressView.setUpdateTextListener(new CircleWaveProgressView.UpdateTextListener() {
            @Override
            public String updateText(float interpolatedTime, float currentProgress, float maxProgress) {
                //取一位整数和并且保留两位小数
                DecimalFormat decimalFormat=new DecimalFormat("0.00");
                String text_value = decimalFormat.format(interpolatedTime * currentProgress / maxProgress * 100)+"%";
                //最终把格式好的内容(数值带进进度条)
                return text_value ;
            }
        });
        //设置进度和动画时间
        circleWaveProgressView.setProgress(50,2500);
    }
}
复制代码

布局文件增加一个 TextView :

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.android.quard.CircleWaveProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

    <TextView
        android:id="@+id/tv_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:textColor="#ffffff"
        android:textSize="24dp"
        />


</android.support.constraint.ConstraintLayout>
复制代码

最终效果如下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

绘制双波浪效果

要实现第二层波浪平移方向和第一层波浪平移方向相反,要改一下绘制顺序,。下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

要从p1开始绘制,因为第二层是要右移,所以右边要绘制多一次波浪,像绘制第一层波浪一样,只不过这次是左边,代码如下:

//是否绘制双波浪线
    private boolean isCanvasSecond_Wave;
    //第二层波浪的颜色
    private int second_WaveColor;
    //第二层波浪的画笔
    private Paint secondWavePaint;
复制代码

attrs文件增加第二层波浪的颜色:

<!--这里的名字要和自定义的View名称一样,不然在xml布局中无法引用-->
    <declare-styleable name="CircleWaveProgressView">
        <!--波浪的颜色-->
        <attr name="wave_color" format="color"></attr>
        <!--圆形背景颜色-->
        <attr name="circlebg_color" format="color"></attr>
        <!--波浪长度-->
        <attr name="wave_length" format="dimension"></attr>
        <!--波浪高度-->
        <attr name="wave_height" format="dimension"></attr>
        <!--当前进度-->
        <attr name="currentProgress" format="float"></attr>
        <!--最大进度-->
        <attr name="maxProgress" format="float"></attr>
        <!--第二层波浪的颜色-->
        <attr name="second_color" format="color"></attr>
    </declare-styleable>
复制代码

类文件:

//第二层波浪的颜色
        second_WaveColor = typedArray.getColor(R.styleable.CircleWaveProgressView_second_color,Color.RED);
复制代码

init 方法增加:

//初始化第二层波浪画笔
        secondWavePaint = new Paint();
        secondWavePaint.setColor(second_WaveColor);
        secondWavePaint.setAntiAlias(true);
        //要覆盖在第一层波浪上,所以选SRC_ATOP模式,第二层波浪完全显示,并且第一层非交集部分显示。这个模式看上面的图像合成图文章就可以了解
        secondWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
        //初始状态不绘制第二层波浪
        isCanvasSecond_Wave = false;
复制代码

onDraw 方法增加:

@Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);

        Log.d("ssd",getWidth()+"");
        //这里用到了缓存 根据参数创建新位图
        circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
        //以该bitmap为低创建一块画布
        bitmapCanvas = new Canvas(circleBitmap);
        //绘制圆形 半径设小了一点,就是为了能让波浪填充完整个圆形背景
        bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2 - Density.dip2px(getContext(),8), circlePaint);
        //绘制波浪形
        bitmapCanvas.drawPath(paintWavePath(),wavePaint);
        //是否绘制第二层波浪
        if(isCanvasSecond_Wave){
            bitmapCanvas.drawPath(cavasSecondPath(),secondWavePaint);

        }
        //裁剪图片
        canvas.drawBitmap(circleBitmap, 0, 0, null);
        //绘制波浪线
      //  canvas.drawPath(paintWavePath(),wavePaint);

    }
    
        //是否绘制第二层波浪
    public void isSetCanvasSecondWave(boolean isCanvasSecond_Wave){
        this.isCanvasSecond_Wave = isCanvasSecond_Wave;
    }

    /**
     * 绘制第二层波浪方法
     * @return
     */
    private Path cavasSecondPath(){
        float secondWaveHeight = waveHeight;
        wavePath.reset();
        //移动到右上方,也就是p1点
        wavePath.moveTo(waveActualSize + moveDistance, (1 - currentPercent) * waveActualSize);
        //p1 - p0
        for(int i = 0; i < waveNumber * 2 ; i ++){
            wavePath.rQuadTo(-waveLength / 2,secondWaveHeight,-waveLength,0);
            wavePath.rQuadTo(-waveLength / 2,-secondWaveHeight,-waveLength,0);
        }
        //p0-p3 p3-p0d的高度随着进度变化而变化
        wavePath.lineTo(0, waveActualSize);
        //连接p3 - p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        //连接p2 - p1
        wavePath.lineTo(waveActualSize,(1 - currentPercent) * waveActualSize);
        //封闭起来填充
        wavePath.close();
        return wavePath;

    }
复制代码

最后在Activty文件设置:

//是否绘制第二层波浪
        circleWaveProgressView.isSetCanvasSecondWave(true);
复制代码

最终效果如下图:

Android教你一步一步从学习贝塞尔曲线到实现波浪进度条

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

增长黑客

增长黑客

范冰 / 电子工业出版社 / 2015-7-1 / CNY 59.00

“增长黑客”这一概念近年来兴起于美国互联网创业圈,最早是由互联网创业者Sean Ellis提出。增长黑客是介于技术和市场之间的新型团队角色,主要依靠技术和数据的力量来达成各种营销目标,而非传统意义上靠砸钱来获取用户的市场推广角色。他们能从单线思维者时常忽略的角度和难以企及的高度通盘考虑影响产品发展的因素,提出基于产品本身的改造和开发策略,以切实的依据、低廉的成本、可控的风险来达成用户增长、活跃度上......一起来看看 《增长黑客》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换