自定义控件之绘图篇(一) —— 概述及基本几何图形绘制
自定义控件之绘图篇(二) —— 路径及文字
自定义控件之绘图篇(三) —— 区域(Range)
自定义控件之绘图篇(四) —— Canvas变换与操作
自定义控件之绘图篇(五) —— drawText()详解
自定义控件之绘图篇(六) —— Path之贝赛尔曲线和手势轨迹、水波纹效果
从这篇开始,我将延续该系列文章把图片相关的知识给大家讲完,这一篇先稍微进阶一下,给大家把《自定义控件之绘图篇(二) —— 路径及文字》略去的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)
前两个是二阶贝赛尔曲线函数,后两个是三阶贝赛尔曲线函数。
这里的四个函数的具体意义我们后面会具体详细讲解,我们这篇也就是利用这四个函数来实现我们的贝赛尔曲线相关的效果的。
在数学的数值分析领域中,贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。
这部分是很有难度的,大家做好准备了哦。
其公式可概括为:
对应动画演示为:
P0为起点,P1为终点,t表示当前时间,B(t)表示公式的结果值。
注意,曲线的意义就是公式结果B(t)随时间的变化,其取值所形成的轨迹。在动画中,黑色点表示在当前时间t下公式B(t)的取值。而红色的那条线就不在各个时间点下不同取值的B(t)所形成的轨迹。
总而言之:对于一阶贝赛尔曲线,大家可以理解为在起始点和终点形成的这条直线上,匀速移动的点。
同样,先来看看二阶贝赛尔曲线的公式(虽然看不懂,呵呵)
这不就是一阶贝赛尔公式的平方嘛!
大家也不用研究这个公式了,没一定数学功底也研究不出来了啥,咱还是看动画吧
在这里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中当然会有它的踪影,它就是钢笔工具,钢笔工具所使用的路径弯曲效果就是二阶贝赛尔曲线,下面我来给大家演示一下钢笔工具的用法
我们拿最终成形的图形来看一下钢笔工具是怎么形成二阶贝赛尔曲线效果的
右图演示的假设某一点t=0.25时,动态点B的位置图。
同样,这里P0是起始点,P2是终点,P1是控制点,P0~P1、P1~P2形成了第一层的一阶贝赛尔曲线。它们随时间的动态点分别是Q0、Q1 ,动态点Q0、Q1又形成了第二层的一阶贝赛尔曲线,它们的动态点是B,而B的轨迹跟钢笔工具的形状是完全一样的,所以钢笔工具的拉伸效果是使用的二阶贝赛尔曲线!
这个图与上面二阶贝赛尔曲线t=0.25时的曲线差不多,大家理解起来难度也不大。
这里需要注意的是,我们在使用钢笔工具时,拖动的是P5点,其实二阶贝赛尔曲线的控制点是其对面的P1点,钢笔工具这样设计当然是因为操作起来比较方便。
好了,对贝赛尔曲线的知识讲了那么多,下面开始实战了,看在代码中,贝赛尔曲线是怎么来做的。
在开篇中,我们已经提到,在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(float, float, float, float)
和rQuadTo(float, float, float, float)
为主,三阶贝赛尔曲线的使用方法与二阶贝赛尔曲线类似,用处也比较少,这篇就不再细讲了。
这部分我们先来看看quadTo(float x1, float y1, float x2, float y2)
函数的用法,参数中(x1, y1)是控制点坐标,(x2, y2)是终点坐标 。
大家可能会有一个疑问:有控制点和终点坐标,那起始点是多少呢?
整条线的起始点是通过Path#moveTo(float x, float y)
来指定的,而如果我们连续调用quadTo(float, float, float, float)
,前一个quadTo(float, float, float, float)
的终点,就是下一个quadTo(float, float, float, float)
函数的起点,如果初始没有调用path.moveTo(x, y)
来指定起始点,则默认以控件左上角(0, 0)为起始点。大家可能还是有点迷糊,下面我们就举个例子来看看我们利用quadTo(float, float, float, float)
来画下面的这条波浪线:
最关键的是如何来确定控制点的位置!前面讲过,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),下面开始是代码部分了。
我们知道在动画绘图时,会调用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(float, float, float, float)
的起始点是前一个path.quadTo(float, float, float, float)
的终点。有关控制点的位置如何查找,我们上面已经利用钢笔工具给大家讲解了,这里就不再细讲。
所以,大家在自定义控件的时候,要多跟UED沟通,看他们是如何来实现这个效果的,如果是用的钢笔工具,那我们也可以效仿使用二阶贝赛尔曲线来实现。
在自定义控件以后,直接把它引入到主布局文件中即可(main.xml)
<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(float x, float y)
来指定的,如果初始没有调用Path#moveTo(float x, float y)
来指定起始点,则默认以控件左上角(0, 0)为起始点。
而如果我们连续调用quadTo(float, float, float, float)
,前一个quadTo(float, float, float, float)
的终点,就是下一个quadTo(float, float, float, float)
函数的起点。
要实现手指轨迹其实是非常简单的,我们只需要自定义拦截事件(onTouchEvent()),然后根据手指的移动轨迹来绘制Path即可。
要实现把手指的移动轨迹连接起来,最简单的方法就是直接使用Path#lineTo(float x, float y)
就能实现把各个点连接起来。
我们先来看看效果图:
首先,我们自定义一个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(float x, float y)
和Path#lineTo(float x, float y)
的用法理解起来应该没什么难度,但这里有两个地方需要注意:
第一,有关在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()
刷新得那么快。
所以在我们确定当前线程是主线程的情况下,还是以invalidate()
函数为主。当我们不确定当前要刷新页面的位置所处的线程是不是主线程的时候,还是用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();
}
然后看看布局文件(mian.xml)
<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>
没什么难度,就是把自定义控件添加到布局中。
然后看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()
来重置画布。
上面我们虽然实现了,画出手指的移动轨迹,但我们仔细来看看画出来的图
我们把S放大,明显看出,在两个点连接处有明显的转折,而且在S顶部位置横纵坐标变化比较快的位置,看起来跟图片打了马赛克一样,利用Path绘图,是不可能出现马赛克的,因为除了Bitmap以外的任何Canvas绘图全部都是矢量图,也就是利用数学公式来作出来的图,无论放在多大屏幕上,都不可能会出现马赛克!这里利用Path绘图,在S顶部之所以看起来像是马赛克是因为这个S是由各个不同点之间连线写出来的,而之间并没有平滑过渡,所以当坐标变化比较剧烈时,线与线之间的转折就显得特别明显了。
所以要想优化这种效果,就得实现线与线之间的平滑过渡,很显然,二阶贝赛尔曲线就是干这个事的。下面我们就利用我们新学的Path#quadTo(float x1, float y1, float x2, float y2)
函数来重新实现下移动轨迹效果。
我们上面讲了,使用Path#lineTo(float x, float y)
的最大问题就是线段转折处不够平滑。Path#quadTo(float x1, float y1, float x2, float y2)
可以实现平滑过渡,但使用Path#quadTo(float x1, float y1, float x2, float y2)
的最大问题是,如何找到起始点和结束点。
下图中,有用绿点表示的三个点,连成的两条直线,很明显他们转折处是有明显折痕的
下面我们在PhotoShop中利用钢笔工具,看如何才能实现这两条线之间的转折
从这两个线段中可以看出,我们使用Path#lineTo(float x, float y)
的时候,是直接把手指触点A、B、C给连起来。
而钢笔工具要实现这三个点间的流畅过渡,就只能将这两个线段的中间点做为起始点和结束点,而将手指的倒数第二个触点B做为控制点。
大家可能会觉得,那这样,在结束的时候,A到P0和P1到C1的这段距离岂不是没画进去?是的,如果Path最终没有close()
的话,这两段距离是被抛弃掉的。因为手指间滑动时,每两个点间的距离很小,所以P1到C之间的距离可以忽略不计。
下面我们就利用这种方法在PhotoShop中求证,在连接多个线段时,是否能行?
在这个图形中,有很多点连成了弯弯曲曲的线段,我们利用上面我们讲的,将两个线段的中间作为二阶贝尔赛曲线的起始点和终点,把上一个手指的位置作为控制点,来看看是否真的能组成平滑的连线,整个连接过程如动画所示
在最终的路径中可看出,各个点间的连线是非常平滑的。从这里也可以看出,在为了实现平滑效果,我们只能把开头的线段一半和结束的线段的一半抛弃掉。
在讲了原理之后,下面就来看看在代码中如何来实现吧。
先贴出完整代码然后再细讲
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(float, float)
的话,会默认是从(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(float, float, float, float)
函数时,就已经说过,第一个起始点是Path#moveTo(float x, float y)
定义的,其它部分,一个quadTo(float, float, float, float)
的终点,是下一个quadTo(float, float, float, float)
的起始点。
所以这里的起始点,就是上一个线段的中间点。这样就与钢笔工具绘制过程完全对上了:把各个线段的中间点做为起始点和终点,把终点前一个手指位置做为控制点。
后面的onDraw()和reset()
函数就没什么难度了,上面的例子中也讲过了,就不再赘述了,最终的效果图如下
同样把lineTo(float, float)
和quadTo(float, float, float, float)
实现的S拿来对比下
从效果图中可以明显看出,通过quadTo(float, float, float, float)
实现的曲线更顺滑
Ok啦,quadTo(float, float, float, float)
的用法,到这里就结束了,下部分再来讲讲rQuadTo(float, float, float, float)
的用法及波浪动画效果
该函数声明如下
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))即(400, 300),同样,得到的终点坐标是(300+200, 400+100)即(500, 500)。 所以下面这两段代码是等价的:
利用quadTo(float, float, float, float)
定义绝对坐标
path.moveTo(300,400);
path.quadTo(400,300,500,500);
与利用rQuadTo(float, float, float, float)
定义相对坐标
path.moveTo(300,400);
path.rQuadTo(100,-100,200,100)
在上篇中,我们使用quadTo(float, float, float, float)
实现了一个简单的波浪线
各个点具体计算过程,在上篇已经计算过了,下面是上篇中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(float, float, float, float)
来重新实现下
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=400;
控制点Y坐标 = 上一个终点Y坐标+控制点Y位移=300+100=400;
终点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)
中的位移坐标,都是以上一个终点位置为基准来做偏移的!
本节完成之后,将实现文章开头的波浪效果,如下
上面我们已经能够实现一个波形,只要我们再多实现几个波形,就可以覆盖整个屏幕了
对应代码如下
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(float, float)
闭合的区域
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(float, float)
的时候,将起始点向右移动即可实现移动,而且只要我们移动一个波长的长度,波纹就会重合,就可以实现无限循环了。 为此我们定义一个动画
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(float, float)
中加上现在的移动值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坐标就可以了,代码比较简单,而且本文实在是太长了,具体实现就不再讲了。
好了,本篇文章到这里就结束了。
原文链接:点击这里