在上面的博客中说了path的绘制,path绘制, 介绍了除了贝塞尔曲线的其他情况。 在这里单独介绍一下贝塞尔曲线。贝塞尔曲线是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝塞尔曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线。 我们再开发中很多都会用到贝塞尔曲线的情况,比如水波纹效果。贝塞尔曲线扫盲贴这一片博客很形象的把它的原理解释了一下。看完这个我们可以比较明白的看出。贝塞尔曲线就是两个固定点确定的情况下,根据一个或者多个控制点通过计算得到的曲线。
-
api分析
这是贝塞尔曲线的数学解释,我们再实现对path绘制的时候,的确不需要知道这个。只要知道大致的形式就行,但是对于后期了解属性动画之后,就有意义了。很多时候都是需要用到这个公式。比如经常出现的直播间花束心形点赞效果,那些心形的移动路线就是按照贝塞尔曲线绘制。
我们通过官方文档来看,关于二次,三次贝塞尔曲线的情况:
/** 从注释中可以看出,x1,y1,就是控制点的坐标,x2,y2就是最后固定点的坐标。
* 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就是控制点的坐标,x3,y3就是末尾固定点的坐标
* Add a cubic bezier from the last point, approaching control points
* (x1,y1) and (x2,y2), and ending at (x3,y3). 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 1st control point on a cubic curve
* @param y1 The y-coordinate of the 1st control point on a cubic curve
* @param x2 The x-coordinate of the 2nd control point on a cubic curve
* @param y2 The y-coordinate of the 2nd control point on a cubic curve
* @param x3 The x-coordinate of the end point on a cubic curve
* @param y3 The y-coordinate of the end point on a cubic curve
*/
public void cubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
isSimplePath = false;
nCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}
我们可以可以做一个例子,通过控制控制点的坐标,来看看贝塞尔曲线的情况 。
图一就是quadTo的应用,图二就是cubicTo的应用。我们可以通过改变控制点的坐标,来改变曲线的样式。代码如下:
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPaint.setStrokeWidth(8);
mPaint.setColor(Color.parseColor("#ff00ff"));
mPath.moveTo(100,400);
mPath.quadTo(mControlPoint.x,mControlPoint.y,1000,400);
canvas.drawPath(mPath,mPaint);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(20);
canvas.drawPoint(100,400,mPaint);
canvas.drawPoint(1000,400,mPaint);
canvas.drawPoint(mControlPoint.x,mControlPoint.y,mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//通过移动改变控制点的坐标,然后重绘,改变贝塞尔曲线的状态
mControlPoint.set((int) event.getX(), (int) event.getY());
invalidate();
return true;
}
-
水波纹效果
我们可以想象水波纹其实就可以看做是两个曲线相互交叉的效果,比如下图所示:
其实这就相当于一个向左移动和一个向右移动的两个曲线,然后和一个圆形相交的部分。 其实绘制所需要的特殊view。就是想它一步步分解的过程,分解成最基本的可以直接通过api绘画的模块。
public class BezierView extends View {
private static final String TAG = ViewPath.class.getSimpleName();
private ValueAnimator mAnimator;
private int currentValue;
private int xOffset;
Paint mPaint;
Point mCenterPoint;
Path mPath;
private int windowWidth;
Path mRightPath;
Path mDesPath;
public BezierView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
//抗锯齿
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
// mPaint.setStyle(Paint.Style.STROKE);
mCenterPoint = new Point();
mCenterPoint.set(500, 100);
mPath = new Path();
mRightPath = new Path();
mDesPath = new Path();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取屏幕宽度
windowWidth = getWidth();
drawRightWave(canvas);
drawLeftWave(canvas);
}
//从右向左移动的曲线
private void drawRightWave(Canvas canvas) {
mRightPath.reset();
mDesPath.reset();
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.parseColor("#7700CDCD"));
mPaint.setAlpha(200);
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.FILL);
//计算真个向左移动的贝塞尔曲线中需要点的个数,
//因为每个贝塞尔曲线中的两个固定点之间的间距是300像素,所以要除以300
//移动的距离这里取的是屏幕宽度的7倍,以当前可见屏幕为标准,向左,右各扩展到3个屏幕宽度
//这个具体的一定距离,可以按需设置,我这里就直接用了一个比较大的值
int pointNumbers = windowWidth*7/300;
mRightPath.moveTo(windowWidth*4 - xOffset, 400);
//从右向左依次把曲线添加到path中,用rQuadTo是因为他总是以path的最后一个点为原点
for (int i = 0; i < pointNumbers+1; i++) {
if (i % 2 == 0) {
mRightPath.rQuadTo(-150, -50, -300, 0);
} else {
mRightPath.rQuadTo(-150, 50, -300, 0);
}
}
//因为有一个圆形在里面所以首先要将path形成一个闭环,因为我们在这里写死圆的位置
//它是一个以400,400为圆心,300为半径的圆,所以我们的mRightPath形成的闭环Y轴坐标应该是
//700, 以保证和圆的底部相切
mRightPath.lineTo(-(windowWidth*3)-xOffset - 300*(pointNumbers+1),700);
mRightPath.lineTo(windowWidth*4 - xOffset,700);
mRightPath.close();
mDesPath.addCircle(400,400,300, Path.Direction.CW);
//将圆形和曲线取交集
mDesPath.op(mRightPath, Path.Op.INTERSECT);
canvas.drawPath(mDesPath, mPaint);
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(400,400,300,mPaint);
}
//从左向右移动的曲线,这个与从右向左类似
private void drawLeftWave(Canvas canvas) {
mPath.reset();
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.parseColor("#7700F5FF"));
mPaint.setAlpha(200);
mPaint.setStyle(Paint.Style.FILL);
mPath.moveTo(-(windowWidth*3) + xOffset, 400);
int pointNumbers = windowWidth*7/300;
for (int i = 0; i < pointNumbers+1; i++) {
if (i % 2 == 0) {
mPath.rQuadTo(100, -50, 200, 0);
} else {
mPath.rQuadTo(100, 50, 200, 0);
}
}
mPath.lineTo(windowWidth*4 + xOffset,700);
mPath.lineTo(-(windowWidth*3) + xOffset,400);
mDesPath.addCircle(400,400,300, Path.Direction.CW);
mDesPath.op(mPath, Path.Op.INTERSECT);
canvas.drawPath(mDesPath, mPaint);
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(400,400,300,mPaint);
}
//设置移动的动画,通过valueanimator进行对连个曲线的移动
private void startAnimator(int start, int end, long animTime) {
mAnimator = ValueAnimator.ofInt(start, end);
mAnimator.setDuration(animTime);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.setRepeatMode(ValueAnimator.RESTART);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
xOffset = value;
currentValue = (int) (value * 0.6);
invalidate();
}
});
mAnimator.start();
}
public void setCurrentValue(int value, int offset) {
startAnimator(0, offset, 1000*2);
}
}
很多进度条需要这样实现 ,如果是进度条的话,那么他的变量就是圆的y轴的位置变化,通过移动两个曲线y轴的坐标来实现这个功能。