Android 浅谈贝塞尔曲线

本文也是自定义控件相关的内容,主要介绍贝塞尔曲线在Android开发中的应用。

首先通过理论介绍一下贝塞尔曲线的基本实现原理,然后借助于图片演示了一下贝塞尔曲线的实现方式。文中更多介绍的是关于二阶和三阶贝塞尔曲线的内容,如果想了解更高阶贝塞尔曲线内容,可以上网查看了解更多。

Android中提供了二阶和三阶贝塞尔曲线的API,借助于这些API可以实现许多炫酷的交互,比如加入购物车、水波纹、点赞或者直播赞赏动效等等。文章末尾部分介绍了如何通过二阶贝塞尔曲线的API实现一个类似加入购物车的动效。

贝塞尔曲线介绍

贝塞尔曲线顾名思义就是一条曲线,而且是一条非常光滑柔顺的曲线。贝塞尔曲线是由法国数学家Pierre Bézier所发明,它之所以这么出名, 就在于其极高的适用性,只需要依据数学公式,就可以绘制精确平滑的曲线。百度百科中有这样一段介绍: 贝塞尔曲线的有趣之处更在于它的“皮筋效应”,也就是说,随着点有规律地移动,曲线将产生皮筋伸引一样的变换,带来视觉上的冲击。QQ未读消息的小红点就是依据贝塞尔曲线实现的橡皮筋一样的特效。

贝塞尔曲线理论基础

这里以一个比较典型的二阶贝塞尔曲线做示例介绍。

  • 步骤一:在平面内选3个不同线的点并且依次用线段连接。如下所示:

  • 步骤二:在AB和BC线段上找出点D和点E,使得 AD/AB = BE/BC

  • 步骤三:连接DE,在DE上寻找点F,F点需要满足:DF/DE = AD/AB = BE/BC

  • 步骤四:最最重要的!根据DE线段和计算公式找出所有的F点,记住是所有的F点,然后将其这些点连接起来。那连接规则是什么?以上图为例,第一个连接点是A-F,第二连接点是A-F1(这个F1必须满足DF1/DE = AD/AB = BE/BC)以此类推,直到最后连接上C点,下面上一个动图加深理解:

那么这个GIF我截下其中的一张图说明,如下图:

动图里的P0、P1、P2分别代表的是上图的:P0 == A;P1 == B;P2 == C。那么这个黑色点,代表的就是F点,绿色线段的2个端点(P0-P1线段上的绿色点,代表是就是D点,P0-P2线段上的绿色点,代表是就是E点)。线段上面点的获取,必须要满足等比关系。一般将B点或者P1点成为控制点。

如下是三阶和四阶贝塞尔曲线的动效。

Android中应用

在Android开发中比较常用的是二阶贝塞尔曲线和三阶贝塞尔曲线,而且官方提供了这两种曲线的API。

简单二阶贝塞尔曲线

先介绍一个简单示例,这样可以更直观演示一下贝塞尔曲线。

通过一个斜对角点使用二阶贝塞尔曲线绘制一条曲线,然后通过移动控制点观察曲线变化情况。

public MyPathView(Context context, AttributeSet attrs, int defStyleAttr) {
	super(context, attrs, defStyleAttr);
	paint = new Paint();
	paint.setAntiAlias(true);
	paint.setStyle(Paint.Style.STROKE);

	path = new Path();
}

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	width = getWidth();
	height = getHeight();
	//
	path.reset();
	paint.setColor(Color.RED);

	// 绘制控制点
	canvas.drawCircle(eventX, eventY, 5, paint);
	paint.setColor(Color.GRAY);
	paint.setStrokeWidth(3);

	// 绘制与控制点的连接线
	canvas.drawLine(0, 0, eventX, eventY, paint);
	canvas.drawLine(eventX, eventY, width, height, paint);
	paint.setStrokeWidth(10);
	paint.setColor(Color.RED);

	// 二阶贝塞尔曲线
	path.quadTo(eventX, eventY, width, height);
	canvas.drawPath(path, paint);
}

public boolean onTouchEvent(MotionEvent event) {
	switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
		case MotionEvent.ACTION_MOVE:
			eventX = (int) event.getX();
			eventY = (int) event.getY();
			break;
	}
	invalidate();
	return true;
}

抛物线示例

一些电商应用加入购物车使用了抛物线轨迹,使用贝塞尔曲线也可以仿写抛物线示例。当然了开发中不一定都采用贝塞尔曲线,也可以使用其它动画方式,比如X方向使用一个匀速的差值器,Y方向使用先加速后减速的差值器。

方式一:PathMeasure

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	path.reset();
	width = getWidth();
	height = getHeight();
	paint.setColor(Color.RED);
	paint.setStyle(Paint.Style.STROKE);

	// 绘制贝塞尔曲线轨迹线
	path.quadTo(width * 9 / 20, height / 160, width, height);
	canvas.drawPath(path, paint);
	pathMeasure = new PathMeasure(path, false);

	path01.reset();
	paint.setColor(Color.BLUE);
	paint.setStyle(Paint.Style.FILL);
	path01.addCircle(eventX, eventY, 50, Path.Direction.CCW);
	// 绘制小球
	canvas.drawPath(path01, paint);
}

@Override
public void onClick(View v) {
	ValueAnimator anim = ValueAnimator.ofFloat(0, pathMeasure.getLength());
	anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
		@Override
		public void onAnimationUpdate(ValueAnimator animation) {
			float value = (Float) animation.getAnimatedValue();
			// 获取当前路径点value坐标
			pathMeasure.getPosTan(value, currentPosition, null);
			eventX = (int) currentPosition[0];
			eventY = (int) currentPosition[1];
			invalidate();
		}
	});
	anim.setDuration(1000);
	anim.setInterpolator(new AccelerateDecelerateInterpolator());
	anim.start();
}

方式二:自定义TypeEvaluator

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	path.reset();
	width = getWidth();
	height = getHeight();
	paint.setColor(Color.RED);
	paint.setStyle(Paint.Style.STROKE);
	// 绘制贝塞尔曲线轨迹线
	path.quadTo(width * 9 / 20, height / 160, width, height);
	canvas.drawPath(path, paint);

	path01.reset();
	paint.setColor(Color.BLUE);
	paint.setStyle(Paint.Style.FILL);
	// 绘制小球
	path01.addCircle(eventX, eventY, 50, Path.Direction.CCW);
	canvas.drawPath(path01, paint);
}

@Override
public void onClick(View v) {
	Point startPosition = new Point(0, 0);
	Point endPosition = new Point(width, height);
	Point controllPoint = new Point(width * 9 / 20, height / 160);
	BezierEvaluator bezierEvaluator = new BezierEvaluator(controllPoint);
	ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator, startPosition, endPosition);
	anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
		@Override
		public void onAnimationUpdate(ValueAnimator animation) {
			// 获取当前点Point
			Point point = (Point) animation.getAnimatedValue();
			eventX = point.x;
			eventY = point.y;
			invalidate();
		}
	});
	anim.setDuration(1000);
	anim.setInterpolator(new AccelerateDecelerateInterpolator());
	anim.start();
}


// 自定义TypeEvaluator
public class BezierEvaluator implements TypeEvaluator {

	private Point controllPoint;
	public BezierEvaluator(Point controllPoint) {
		this.controllPoint = controllPoint;
	}

	@Override
	public Point evaluate(float t, Point startValue, Point endValue) {
		int x = (int) ((1 - t) * (1 - t) * startValue.x + 2 * t * (1 - t) * controllPoint.x + t * t * endValue.x);
		int y = (int) ((1 - t) * (1 - t) * startValue.y + 2 * t * (1 - t) * controllPoint.y + t * t * endValue.y);
		return new Point(x, y);
	}
}

上述两种实现方式的效果图是一致的。

小结

本文内容不多,就是整理了一下贝塞尔曲线的基础点,通过前面的几张图片应该很容易理解什么是贝塞尔曲线。最初接触贝塞尔曲线时,感觉是一个相当高大上的角色,然后慢慢接触多了,理解了基本原理,再次自己使用时,会发现原来就是这样的。作为应用开发人员,个人认为不必太专注于这里的公式原理,公式原理都是已经被专业人员证明使用多次了,我们应该更专注于使用层面即可。

后面又借助于Android贝塞尔曲线API实现了两个简单示例,示例都是借助于二阶贝塞尔曲线实现的,一般开发中使用二阶或者三阶比较多,当然了公式也不必刻意去记忆,在使用的时候,如果系统定义不能满足需要,也可以直接根据理论公式自己去实现。

有关如果通过贝塞尔曲线实现水波纹加载效果,会在下一篇博文中介绍,下一篇博文中主要介绍水波纹加载动画效果,到时会使用正弦和贝塞尔曲线方法对比看一下实现效果。

评论

您确定要删除吗?删除之后不可恢复