自定义控件三部曲之绘图篇(六)——Path之贝赛尔曲线和手势轨迹、水波纹效果

前言:好想义无反顾地追逐梦想
html

相关文章:
《Android自定义控件三部曲文章索引》http://blog.csdn.net/harvic880925/article/details/50995268

从这篇开始,我将延续androidGraphics系列文章把图片相关的知识给你们讲完,这一篇先稍微进阶一下,给你们把《android Graphics(二):路径及文字》略去的quadTo(二阶贝塞尔)函数,给你们补充一下。
本篇最终将以两个例子给你们演示贝塞尔曲线的强大用途:
一、手势轨迹

java

利用贝塞尔曲线,咱们能实现平滑的手势轨迹效果
二、水波纹效果
android


电池充电时,有些手机会显示水波纹效果,就是这样作出来的。
废话很少说,开整吧
算法

1、概述

《android Graphics(二):路径及文字》中咱们略去了有关全部贝赛尔曲线的知识,在Path中有四个函数与贝赛尔曲线有关:
canvas

//二阶贝赛尔
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)
这里的四个函数的具体意义咱们后面会具体详细讲解,咱们这篇也就是利用这四个函数来实现咱们的贝赛尔曲线相关的效果的。

一、贝赛尔曲线来源

在数学的数值分析领域中,贝赛尔曲线(Bézier曲线)是电脑图形学中至关重要的参数曲线。更高维度的普遍化贝塞尔曲线就称做贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所普遍发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。
ide

二、贝赛尔曲线公式

这部分是颇有难度的,你们作好准备了哦
函数

一阶贝赛尔曲线

其公式可归纳为:

工具

对应动画演示为:
布局


P0为起点、P1为终点,t表示当前时间,B(t)表示公式的结果值。
注意,曲线的意义就是公式结果B(t)随时间的变化,其取值所造成的轨迹。在动画中,黑色点表示在当前时间t下公式B(t)的取值。而红色的那条线就不在各个时间点下不一样取值的B(t)所造成的轨迹。
总而言之:对于一阶贝赛尔曲线,你们能够理解为在起始点和终点造成的这条直线上,匀速移动的点。
post

二阶贝赛尔曲线

一样,先来看看二阶贝赛尔曲线的公式(虽然看不懂,呵呵)


你们也不用研究这个公式了,没必定数学功底也研究不出来了啥,咱仍是看动画吧


在这里P0是起始点,P2是终点,P1是控制点
假设将时间定在t=0.25的时刻,此时的状态以下图所示:


首先,P0点和P1点造成了一条贝赛尔曲线,还记得咱们上面对一阶贝赛尔曲线的总结么:就是一个点在这条直线上作匀速运动;因此P0-P1这条直线上的移动的点就是Q0;
一样,P1,P2造成了一条一阶贝赛尔曲线,在这条一阶贝赛尔曲线上,它们的随时间移动的点是Q1
最后,动态点Q0和Q1又造成了一条一阶贝赛尔曲线,在它们这条一阶贝赛尔曲线动态移动的点是B
而B的移动轨迹就是这个二阶贝赛尔曲线的最终形态。从上面的讲解你们也能够知道,之因此叫它二阶贝赛尔曲线是由于,B的移动轨迹是创建在两个一阶贝赛尔曲线的中间点Q0,Q1的基础上的。
在理解了二阶贝赛尔曲线的造成原理之后,咱们就不难理解三阶贝赛尔曲线了

三阶贝赛尔曲线

一样,先列下基本看不懂的公式


这玩意估计也看不懂,讲了也没什么意义,仍是结合动画来吧


一样,咱们取其中一点来说解轨迹的造成原理,当t=0.25时,此时状态以下:


一样,P0是起始点,P3是终点;P1是第一个控制点,P2是第二个控制点;
首先,这里有三条一阶贝赛尔曲线,分别是P0-P1,P1-P2,P2-P3;
他们随时间变化的点分别为Q0,Q1,Q2
而后是由Q0,Q1,Q2这三个点,再次链接,造成了两条一阶贝赛尔曲线,分别是Q0—Q1,Q1—Q2;他们随时间变化的点为R0,R1
一样,R0和R1一样能够链接造成一条一阶贝赛尔曲线,在R0—R1这条贝赛尔曲线上随时间移动的点是B
而B的移动轨迹就是这个三阶贝赛尔曲线的最终形状。
从上面的解析你们能够看出,所谓几阶贝赛尔曲线,所有是由一条条一阶贝赛尔曲线搭起来的;
在上图中,造成一阶贝赛尔曲线的直线是灰色的,造成二阶贝赛尔曲线线是绿色的,造成三阶贝赛尔曲线的线是蓝色的。
在理解了上面的二阶和三阶贝赛尔曲线之后,咱们再来看几个贝赛尔曲线的动态图

四阶贝赛尔曲线


五阶贝赛尔曲线


这里就再也不一一讲解造成原理了,你们理解了二阶和三阶贝赛尔曲线之后,这两条的看看就行了,想必你们也是能本身推出四阶贝赛尔曲线的造成原理的。

三、贝赛尔曲线与PhotoShop钢笔工具

若是有些同窗不懂PhotoShop,这篇文章可能就会有些难度了,本篇文章主要是利用PhotoShop的钢笔工具来得出具体贝塞尔图像的
这么屌的贝赛尔曲线,在专业绘图工具PhotoShop中固然会有它的踪迹,它就是钢笔工具,钢笔工具所使用的路径弯曲效果就是二阶贝赛尔曲线。
我来给你们演示一下钢笔工具的用法:


咱们拿最终成形的图形来看一下为何钢笔工具是二阶贝赛尔曲线:


右图演示的假设某一点t=0.25时,动态点B的位置图
一样,这里P0是起始点,P2是终点,P1是控制点;
P0-P一、P1-P2造成了第一层的一阶贝赛尔曲线。它们随时间的动态点分别是Q0,Q1
动态点Q0,Q1又造成了第二层的一阶贝赛尔曲线,它们的动态点是B.而B的轨迹跟钢笔工具的形状是彻底同样的。因此钢笔工具的拉伸效果是使用的二阶贝赛尔曲线!
这个图与上面二阶贝赛尔曲线t=0.25时的曲线差很少,你们理解起来难度也不大。
这里须要注意的是,咱们在使用钢笔工具时,拖动的是P5点。其实二阶贝赛尔曲线的控制点是其对面的P1点,钢笔工具这样设计是固然是由于操做起来比较方便。
好了,对贝赛尔曲线的知识讲了那么多,下面开始实战了,看在代码中,贝赛尔曲线是怎么来作的。

2、Android中贝赛尔曲线之quadTo

在开篇中,咱们已经提到,在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为主,三阶贝赛尔曲线cubicTo、rCubicTo用的使用方法与二阶贝赛尔曲线相似,用处也比较少,这篇就再也不细讲了。

一、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()来画下面的这条波浪线:

最关键的是如何来肯定控制点的位置!前面讲过,PhotoShop中的钢笔工具是二阶贝赛尔曲线,因此咱们能够利用钢笔工具来模拟画出这条波浪线来辅助肯定控制点的位置


下面咱们来看看这个路径轨迹中,控制点分别在哪一个位置


咱们先看P0-P2这条轨迹,P0是起点,假设位置坐标是(100,300),P2是终点,假充位置坐标是(300,300);在以P0为起始点,P2为终点这条二阶贝赛尔曲线上,P1是控制点,很明显P1大概在P0,P2中间的位置,因此它的X坐标应该是200,关于Y坐标,咱们没法肯定,但很明显的是P1在P0,P2点的上方,也就是它的Y值比它们的小,因此根据钢笔工具上面的位置,咱们让P1的比P0,P2的小100;因此P1的坐标是(200,200)
同理,不难求出在P2,P4这条二阶贝赛尔曲线上,它们的控制点P3的坐标位置应该是(400,400);P3的X坐标是400是,由于P3点是P2,P4的中间点;与P3与P1距离P0-P2-P4这条直线的距离应该是相等的。P1距离P0-P2的值为100;P3距离P2-P4的距离也应该是100,这样不难算出P3的坐标应该是(400,400);
下面开始是代码部分了。

二、示例代码

(1)、自定义View

咱们知道在动画绘图时,会调用onDraw(Canvas canvas)函数,咱们若是重写了onDraw(Canvas canvas)函数,那么咱们利用canvas在上面画了什么,就会显示什么。因此咱们自定义一个View

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

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

    @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的终点为起始点的。有关控制点的位置如何查找,咱们上面已经利用钢笔工具给你们讲解了,这里就再也不细讲。
因此,你们在自定义控件的时候,要多跟UED沟通,看他们是如何来实现这个效果的,若是是用的钢笔工具,那咱们也能够效仿使用二阶贝赛尔曲线来实现。

二、使用MyView

在自定义控件之后,而后直接把它引入到主布局文件中便可(main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">

    <com.harvic.BlogBerzMovePath.MyView
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</LinearLayout>
因为直接作为控件显示,因此MainActivity不须要额外的代码便可显示,MainActivity代码以下:
public class MyActivity extends Activity {
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}
源码在文章底部给出
经过这个例子但愿你们知道两点

  • 整条线的起始点是经过Path.moveTo(x,y)来指定的,若是初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;
  • 而若是咱们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;

3、手指轨迹

要实现手指轨迹实际上是很是简单的,咱们只须要在自定义中拦截OnTouchEvent,而后根据手指的移动轨迹来绘制Path便可。
要实现把手指的移动轨迹链接起来,最简单的方法就是直接使用Path.lineTo()就能实现把各个点链接起来。

一、实现方式一:Path.lineTo(x,y)

咱们先来看看效果图:

(1)、自定义View——MyView

首先,咱们自定义一个View,完整代码以下:

public class MyView extends View {

    private Path mPath = new Path();
    public MyView(Context context) {
        super(context);
    }

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

    @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.GREEN);
        paint.setStyle(Paint.Style.STROKE);

        canvas.drawPath(mPath,paint);
    }

    public void reset(){
        mPath.reset();
        invalidate();
    }
}
最重要的位置就是在重写onTouchEvent的位置:
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);
}
当用户点击屏幕的时候,咱们调用mPath.moveTo(event.getX(), event.getY());而后在用户移动手指时使用mPath.lineTo(event.getX(), event.getY());将各个点串起来。而后调用postInvalidate()重绘;
Path.moveTo()和Path.lineTo()的用法,你们若是看了 《android Graphics(二):路径及文字》以后,理解起来应该没什么难度,但这里有两个地方须要注意
第一:有关在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为好;
这里我是故意用的postInvalidate(),由于onTouchEvent()原本就是在主线程中的,使用Invalidate()是更合适的。当咱们
有关OnDraw函数就没什么好讲的,就是把path给画出来:
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint();
    paint.setColor(Color.GREEN);
    paint.setStyle(Paint.Style.STROKE);

    canvas.drawPath(mPath,paint);
}
最后,我还额外写了一个重置函数:
public void reset(){
    mPath.reset();
    invalidate();
}

(2)、主布局

而后看看布局文件(mian.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
    <Button
            android:id="@+id/reset"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="reset"/>

    <com.harvic.BlogMovePath.MyView
            android:id="@+id/myview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</LinearLayout>
没什么难度,就是把自定义控件添加到布局中

(3)、MyActivity

而后看MyActivity的操做:

public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        final MyView myView = (MyView)findViewById(R.id.myview);
        findViewById(R.id.reset).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                myView.reset();
            }
        });
    }
}
这里实现的就是当点击按钮时,调用 myView.reset()来重置画布;
源码在文章底部给出

(4)、使用Path.lineTo()所存在问题

上面咱们虽然实现了,画出手指的移动轨迹,但咱们仔细来看看画出来的图:

咱们把S放大,明显看出,在两个点链接处有明显的转折,并且在S顶部位置横纵坐标变化比较快的位置,看起来跟图片这大后的马赛克同样;利用Path绘图,是不可能出现马赛克的,由于除了Bitmap之外的任何canvas绘图所有都是矢量图,也就是利用数学公式来做出来的图,不管放在多大屏幕上,都不可能会出现马赛克!这里利用Path绘图,在S顶部之因此看起来像是马赛克是由于这个S是由各个不一样点之间连线写出来的,而之间并无平滑过渡,因此当坐标变化比较剧烈时,线与线之间的转折就显得特别明显了。
因此要想优化这种效果,就得实现线与线之间的平滑过渡,很显然,二阶贝赛尔曲线就是干这个事的。下面咱们就利用咱们新学的Path.quadTo函数来从新实现下移动轨迹效果。

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

(1)、原理概述

咱们上面讲了,使用Path.lineTo()的最大问题就是线段转折处不够平滑。Path.quadTo()能够实现平滑过渡,但使用Path.quadTo()的最大问题是,如何找到起始点和结束点。
下图中,有用绿点表示的三个点,连成的两条直线,很明显他们转折处是有明显折痕的

下面咱们在PhotoShop中利用钢笔工具,看如何才能实现这两条线之间的转折



从这两个线段中能够看出,咱们使用Path.lineTo()的时候,是直接把手指触点A,B,C给连起来。
而钢笔工具要实现这三个点间的流畅过渡,就只能将这两个线段的中间点作为起始点和结束点,而将手指的倒数第二个触点B作为控制点。
你们可能会以为,那这样,在结束的时候,A到P0和P1到C1的这段距离岂不是没画进去?是的,若是Path最终没有close的话,这两段距离是被抛弃掉的。由于手指间滑动时,每两个点间的距离很小,因此P1到C之间的距离能够忽略不计。
下面咱们就利用这种方法在photoshop中求证,在链接多个线段时,是否能行?


在这个图形中,有不少点连成了弯弯曲曲的线段,咱们利用上面咱们讲的,将两个线段的中间作为二阶贝尔赛曲线的起始点和终点,把上一个手指的位置作为控制点,来看看是否真的能组成平滑的连线
整个链接过程如动画所示:


在最终的路径中看来,各个点间的连线是很是平滑的。从这里也能够看出,在为了实现平滑效果,咱们只能把开头的线段一半和结束的线段的一半抛弃掉。
在讲了原理以后,下面就来看看在代码中如何来实现吧。

(2)、自定义View

先贴出完整代码而后再细讲:

public class MyView extends View {
    private Path mPath = new Path();
    private float mPreX,mPreY;

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

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

    @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();
                invalidate();
            }
            break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

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

        canvas.drawPath(mPath,paint);
    }

    public void reset(){
        mPath.reset();
        postInvalidate();
    }
}
最难的部分依然是onTouchEvent函数这里:
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;
        }
        …………
    }
    return super.onTouchEvent(event);
}
在ACTION_DOWN的时候,利用 mPath.moveTo(event.getX(),event.getY())将Path的初始位置设置到手指的触点处,若是不调用mPath.moveTo的话,会默认是从(0,0)开始的。而后咱们定义两个变量mPreX,mPreY来表示手指的前一个点。咱们经过上面的分析知道,这个点是用来作控制点的。最后return true让ACTION_MOVE,ACTION_UP事件继续向这个控件传递。
在ACTION_MOVE时:
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();
    invalidate();
}
咱们先找到结束点,咱们说告终束点是这个线段的中间位置,因此很容易求出它的坐标endX,endY;控制点是上一个手指位置即mPreX,mPreY;那有些同窗可能会问了,那起始点是哪啊。在开篇讲quadTo()函数时,就已经说过,第一个起始点是Path.moveTo(x,y)定义的,其它部分,一个quadTo的终点,是下一个quadTo的起始点。
因此这里的起始点,就是上一个线段的中间点。因此,这样就与钢笔工具绘制过程彻底对上了:把各个线段的中间点作为起始点和终点,把终点前一个手指位置作为控制点。
后面的onDraw()和reset()函数就没什么难度了,上面的例子中也讲过了,就再也不赘述了
最终的效果图以下:

一样把lineTo和quadTo实现的S拿来对比下:

从效果图中能够明显能够看出,经过quadTo实现的曲线更顺滑
源码在文章底部给出
Ok啦,quadeTo的用法,到这里就结束了,下部分再来说讲rQuadTo的用法及波浪动画效果


4、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坐标的位移值。可为负值,正值表示相加,负值表示相减;

这四个参数都是传递的都是相对值,相对上一个终点的位移值。
好比,咱们上一个终点坐标是(300,400)那么利用rQuadTo(100,-100,200,100);
获得的控制点坐标是(300+100,400-100)即(500,300)
一样,获得的终点坐标是(300+200,400+100)即(500,500)
因此下面这两段代码是等价的:
利用quadTo定义绝对坐标

path.moveTo(300,400);
path.quadTo(500,300,500,500);
与利用rQuadTo定义相对坐标
path.moveTo(300,400);
path.rQuadTo(100,-100,200,100)

二、使用rQuadTo实现波浪线

在上篇中,咱们使用quadTo实现了一个简单的波浪线:


各个点具体计算过程,在上篇已经计算过了,下面是上篇中onDraw的代码:

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);
}
下面咱们将它转化为rQuadTo来从新实现下:
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.rQuadTo(100,-100,200,0);
    path.rQuadTo(100,100,200,0);
    canvas.drawPath(path,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);
第一句:path.rQuadTo(100,-100,200,0);是创建在(100,300)这个点基础上来计算相对坐标的。
因此
控制点X坐标=上一个终点X坐标+控制点X位移 = 100+100=200;
控制点Y坐标=上一个终点Y坐标+控制点Y位移 = 300-100=200;
终点X坐标 = 上一个终点X坐标+终点X位移 = 100+200=300;
终点Y坐标 = 上一个终点Y坐标+控制点Y位移 = 300+0=300;
因此这句与path.quadTo(200,200,300,300);对等的

第二句:path.rQuadTo(100,100,200,0);是创建在它的前一个终点即(300,300)的基础上来计算相对坐标的!
因此
控制点X坐标=上一个终点X坐标+控制点X位移 = 300+100=200;
控制点Y坐标=上一个终点Y坐标+控制点Y位移 = 300+100=200;
终点X坐标 = 上一个终点X坐标+终点X位移 = 300+200=500;
终点Y坐标 = 上一个终点Y坐标+控制点Y位移 = 300+0=300;
因此这句与path.quadTo(400,400,500,300);对等的

最终效果也是同样的。
经过这个例子,只想让你们明白一点:rQuadTo(float dx1, float dy1, float dx2, float dy2)中的位移坐标,都是以上一个终点位置为基准来作偏移的!

5、实现波浪效果

本节完成以后,将实现文章开头的波浪效果,以下。

一、实现全屏波纹

上面咱们已经可以实现一个波形,只要咱们再多实现几个波形,就能够覆盖整个屏幕了。

对应代码以下:

public class MyView extends View {
    private Paint mPaint;
    private Path mPath;
    private int mItemWaveLength = 400;
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        int originY = 300;
        int halfWaveLen = mItemWaveLength/2;
        mPath.moveTo(-mItemWaveLength,originY);
        for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
            mPath.rQuadTo(halfWaveLen/2,-50,halfWaveLen,0);
            mPath.rQuadTo(halfWaveLen/2,50,halfWaveLen,0);
        }

        canvas.drawPath(mPath,mPaint);
    }
}
最难的部分依然是在onDraw函数中:
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPath.reset();
    int originY = 300;
    int halfWaveLen = mItemWaveLength/2;
    mPath.moveTo(-mItemWaveLength,originY);
    for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
        mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
        mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
    }
    canvas.drawPath(mPath,mPaint);
}
咱们将mPath的起始位置向左移一个波长:
mPath.moveTo(-mItemWaveLength,originY);
而后利用for循环画出当前屏幕中可能容得下的全部波:
for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
    mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
    mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);画的是一个波长中的前半个波,mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);画的是一个波长中的后半个波。你们在这里能够看到,屏幕左右都多画了一个波长的图形。这是为了波形移动作准备的。
到这里,咱们是已经能画出来一整屏幕的波形了,下面咱们把总体波形闭合起来

其中,图中红色区域是我标出来利用lineTo闭合的区域

public class MyView extends View {
    private Paint mPaint;
    private Path mPath;
    private int mItemWaveLength = 400;
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        int originY = 300;
        int halfWaveLen = mItemWaveLength/2;
        mPath.moveTo(-mItemWaveLength+dx,originY);
        for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
            mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
            mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
        }
        mPath.lineTo(getWidth(),getHeight());
        mPath.lineTo(0,getHeight());
        mPath.close();

        canvas.drawPath(mPath,mPaint);
    }
}
这段代码相比上面的代码,增长了两部份内容:
第一,将paint设置为填充:mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
第二,将path闭合:
mPath.moveTo(-mItemWaveLength+dx,originY);
for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
    mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
    mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
}
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();

二、实现移动动画

让波纹动起来其实挺简单,利用调用在path.moveTo的时候,将起始点向右移动便可实现移动,并且只要咱们移动一个波长的长度,波纹就会重合,就能够实现无限循环了。
为此咱们定义一个动画:

public void startAnim(){
    ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);
    animator.setDuration(2000);
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.setInterpolator(new LinearInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            dx = (int)animation.getAnimatedValue();
            postInvalidate();
        }
    });
    animator.start();
}
动画的长度为一个波长,将当前值保存在类的成员变量dx中;
而后在画图的时候,在path.moveTo()中加上如今的移动值dx:mPath.moveTo(-mItemWaveLength+dx,originY);
完整的绘图代码以下:
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPath.reset();
    int originY = 300;
    int halfWaveLen = mItemWaveLength/2;
    mPath.moveTo(-mItemWaveLength+dx,originY);
    for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
        mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
        mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
    }
    mPath.lineTo(getWidth(),getHeight());
    mPath.lineTo(0,getHeight());
    mPath.close();

    canvas.drawPath(mPath,mPaint);
}
完整的MyView代码以下:
public class MyView extends View {
    private Paint mPaint;
    private Path mPath;
    private int mItemWaveLength = 400;
    private int dx;
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        int originY = 300;
        int halfWaveLen = mItemWaveLength/2;
        mPath.moveTo(-mItemWaveLength+dx,originY);
        for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
            mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);
            mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);
        }
        mPath.lineTo(getWidth(),getHeight());
        mPath.lineTo(0,getHeight());
        mPath.close();

        canvas.drawPath(mPath,mPaint);
    }

    public void startAnim(){
        ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);
        animator.setDuration(2000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                dx = (int)animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }
}
而后在MyActivity中开始动画:
public class MyActivity extends Activity {
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final MyView myView = (MyView)findViewById(R.id.myview);
        myView.startAnim();
    }
}
这样就实现了动画:

若是把波长设置为1000,就能够实现本段开篇的动画了。
若是想让波纹像开篇时那要同时向下移动,你们只须要在path.moveTo(x,y)的时候,经过动画同时移动y坐标就能够了,代码比较简单,并且本文实在是太长了,具体实现就再也不讲了,你们能够在源码中加以尝试。
源码在文章底部给出
好了,本篇文章到这里就结束了


若是本文有帮到你,记得加关注哦

源码下载地址:http://download.csdn.net/detail/harvic880925/9476153

请你们尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/50995587 谢谢