上一篇咱们说了 Path 的基本操做,这一篇让咱们来讲一下 Path 的进阶用法——贝塞尔曲线。html
那什么是贝塞尔曲线?贝塞尔曲线能在 Android 中实现什么效果?以及如何作到的?这篇文章都会告诉你。git
贝塞尔曲线是由皮埃尔·贝塞尔发表的,他主要应用于汽车的主体进行设计,后来成为计算机图形学至关重要的参数曲线。github
贝塞尔曲线由什么组成的?它一般由数据点和控制点两个部分组成的。那什么是数据点和控制点呢?请看下表:canvas
类型 | 做用 |
---|---|
数据点 | 曲线的起点和终点 |
控制点 | 控制曲线的弯曲程度 |
这样听起来可能还有点抽象,咱们直接上图来看看。bash
一阶贝塞尔曲线其实就是一条直线,没有控制点,只有数据点 P0,P1,以下图:ide
Android提供方法:lineTo()oop
二阶贝塞尔曲线有一个控制点 P1 和两个数据点 P0,P2。以下图: post
Android 提供方法:quadTo()动画
三阶贝塞尔曲线有两个控制点 P1,P2 和两个数据点 P0,P3。以下图:网站
Android 提供方法:cubicTo()
更高阶的曲线 Android 并无提供 API ,因此在这只会介绍二阶和三阶曲线,若是对更高阶的曲线有兴趣的话,能够去贝塞尔曲线———维基百科和贝塞尔曲线动态演示这两个网站多了解一下。
那么这条曲线到底是怎么造成的呢?先从二阶曲线分析一下:
1.链接 A,B 造成 AB 线段,链接 B,C 造成 BC 线段。
2.在 AB 线段取一个点 D,BC 线段取一个点 E ,使其知足条件: AD/AB = BE/BC,链接 D,E 造成线段 DE。
3.在 DE 取一个点 F,使其知足条件:AD/AB = BE/BC = DF/DE。
4.而知足这些条件的全部的F点所造成的轨迹就是二阶贝塞尔曲线,动态过程以下:
1.链接 A,B 造成 AB 线段,链接 B,C 造成 BC 线段,链接 C,D 造成 CD 线段。
2.在AB线段取一个点 E,BC 线段取一个点 F,CD 线段取一个点 G,使其知足条件: AE/AB = BF/BE = CG/CD。链接 E,F 造成线段 EF,链接 F,G 造成线段 FG。
3.在EF线段取一个点 H,FG 线段取一个点 I,使其知足条件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG。链接 H,I 造成线段 HI。
4.在 HI 线段取一个点 J,使其知足条件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG = HJ/HI。
5.而知足这些条件的全部的J点所造成的轨迹就是三阶贝塞尔曲线,动态过程以下:
说了这么多原理,是时候要知道要怎么运用贝塞尔曲线了。这里我会用两个例子来讲明二阶和三阶贝塞尔曲线的运用:
public void quadTo (float x1, float y1, float x2, float y2)
复制代码
画出二阶贝塞尔曲线
由于二阶贝塞尔曲线须要三个点才能肯定,因此quadTo方法中的四个参数分别是肯定第二,第三的点的。第一个点就是path上次操做的点。 如今用一个实例来练习下这个方法:
如今分步骤来讲明:
咱们首先要画出两段波纹。一段波纹就包含两条曲线。每条曲线咱们可使用 quadTo() 方法来画。
为了更容易理解,请看下图:
mWL 是一段波纹的长度,mCenterY 是屏幕高度的一半。
mPath.moveTo(-mWL, mCenterY); //将path操做的起点移动到(-mWL,mCenterY)
mPath.quadTo((-mWL * 3 / 4) , mCenterY + 60, (-mWL / 2), mCenterY); //画出第一段波纹的第一条曲线
复制代码
mPath.quadTo((-mWL / 4) , mCenterY - 60, 0, mCenterY); //画出第一段波纹的第二条曲线
复制代码
mPath.quadTo((mWL /4) , mCenterY + 60, (mWL / 2), mCenterY); //画出第二段波纹的第一条曲线
复制代码
mPath.quadTo((mWL * 3/ 4) , mCenterY - 60, mWL, mCenterY); //画出第二段波纹的第二条曲线
复制代码
那么如今来想一下应该怎么让这几段波纹动起来呢?咱们须要一个 offset 的平移值。这个值应该加在每一个点的x坐标上,而且 offset 是不断变化的,这样才会造成一个向右平移的效果。那怎么才能获取到这个变化的offset的值呢?答案就是要使用 ValueAnimator 。用法以下:
ValueAnimator animator = ValueAnimator.ofInt(0, mWL); //mWL是一段波纹的长度
animator.setDuration(1000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offset = (Integer) animation.getAnimatedValue(); //offset 的值的范围在[0,mWL]之间。
postInvalidate();
}
});
animator.start();
}
复制代码
这样只要动画开始,offset 就会不断从 0~mWL 变化。
如今为曲线的全部 X 坐标都加上 offset 值。这样就会产平生移的效果,为了简化代码,这里使用的 for 循环来画曲线。
mPath.moveTo(-mWL + mOffset, mCenterY);
for (int i = 0; i < 2; i++) {
mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
}
复制代码
接下来为了适配各类屏幕,须要根据手机的宽度来计算出所须要的波纹的数目:
mWaveCount = (int) Math.round(mScreenWidth / mWL + 1.5); //这样就保证波纹能覆盖整个屏幕
复制代码
上面的 for 循环也能够改成:
mPath.moveTo(-mWL + mOffset, mCenterY);
for (int i = 0; i < mWaveCount; i++) {
mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
}
复制代码
咱们首先要知道怎么使用 cubicTo() 方法来画个半径为 r 的正圆。其实使用 cubicTo() 来画正圆就须要 4 条三阶贝塞尔曲线组合而成。如图所示:
若是要画 P0P3 那道曲线应该怎么画呢?咱们就要知道 P0,P1,P2,P3 这四个点的坐标。P0,P3 的坐标咱们已经知道了,分别是 (0,-r),(r,0)。那么 P1 和 P2 的坐标是什么呢?其实这里有个论证的过程,这个过程在这篇文章就有:Approximate a circle with cubic Bézier curves,感兴趣的能够看看。这里只说结果,最后获得一个数,这个数就是 c = 0.551915024494。也就是说 P1,P2 的坐标就是 (cr,-r),(r,-cr)。其余点的坐标也是用一样的方法得出的,这里就不细说了。
为了更方便管理这几个点,我将这几个点封装分红两个类。分别是 HorizontalLine 和 VerticalLine 。圆的上下两条线属于 HorizontalLine,圆的左右两条线属于 VerticalLine。
如下是这两个类的代码:
private float c = 0.551915024494f;
class HorizontalLine {
public PointF left = new PointF(); //P7 P11
public PointF middle = new PointF(); //P0 P6
public PointF right = new PointF(); //P1 P5
public HorizontalLine(float x,float y) {
left.x = -radius*c;
left.y = y;
middle.x = x;
middle.y = y;
right.x = radius*c;
right.y = y;
}
public void setY(float y) {
left.y = y;
middle.y = y;
right.y = y;
}
}
class VerticalLine {
public PointF top = new PointF(); //P2 P10
public PointF middle = new PointF(); //P3 P9
public PointF bottom = new PointF(); //P4 P8
public VerticalLine(float x,float y) {
top.x = x;
top.y = -radius*c;
middle.x = x;
middle.y = y;
bottom.x = x;
bottom.y = radius*c;
}
public void setX(float x) {
top.x = x;
middle.x = x;
bottom.x = x;
}
}
复制代码
如下是用cubicTo()方法画圆的代码:
private Paint mPaint;
private Path mPath;
private int mScreenHeight;//屏幕高度
private int mScreenWidth;//屏幕宽度
private float radius = 100;
private void initPaint() {
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#59c3e2"));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
private void initPoint() {
mTopLine = new HorizontalLine(0,-radius);
mBottomLine = new HorizontalLine(0,radius);
mLeftLine = new VerticalLine(-radius,0);
mRightLine = new VerticalLine(radius,0);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
//将画布移动到屏幕正中间
canvas.translate(mScreenWidth / 2, mScreenHeight / 2);
mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
mRightLine.middle.x,mRightLine.middle.y);
mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
mBottomLine.middle.x,mBottomLine.middle.y);
mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
mLeftLine.middle.x,mLeftLine.middle.y);
mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
mTopLine.middle.x,mTopLine.middle.y);
canvas.drawPath(mPath,mPaint);
}
复制代码
想要达到圆收缩的效果只要增长和减小某些坐标就能够了。好比我要达成如图的这种效果,应该怎么作呢?
只要增长 P2,P3,P4 的横坐标,就能够达到这种效果。
如今试试把圆收缩起来:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
//将画布移动到手机屏幕的正中间
canvas.translate(mScreenWidth / 2, mScreenHeight / 2);
//将右边的线的点的横坐标都增大
mRightLine.setX(radius * 1.5f);
mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
mRightLine.middle.x,mRightLine.middle.y);
mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
mBottomLine.middle.x,mBottomLine.middle.y);
mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
mLeftLine.middle.x,mLeftLine.middle.y);
mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
mTopLine.middle.x,mTopLine.middle.y);
canvas.drawPath(mPath,mPaint);
}
复制代码
以此类推,若是要达到刚刚那个动图的效果,就要减小上下两条线的点的纵坐标,而后不断平移画布就能够了。具体代码能够下载源码来看。
源码下载:
参考资料: