Android 贝塞尔曲线简单应用(一)

最近发现四大基础动画(缩放、旋转、位移、透明)在使用时总觉得少了些平滑和过度的感觉。之后看了很多成功的精品APP的实现又查了一些相关的资料。发现很多很炫酷的动画都是通过贝塞尔曲线实线的,而使用贝塞尔曲线有个很明显的好处,那就是动画看着会让人觉得不舒服,而不是传统的给人一种突兀的感觉,同时也更符合当下圆滑的设计理念。

Android中想要画一条贝塞尔曲线是非常简单且容易的,熟悉自定义View的应该都用到过drawPath()方法,简而言之该方法会通过指定的Path路径画出各种你能想到的二维图形(三维的起码我没画过,估计也没有人会用原生去画3d图形吧)。代码如下:

Path path = new Path();
path.moveTo(0, getMeasuredHeight());
path.lineTo(0, getMeasuredHeight() – rectHeight);
path.quadTo(MyApplication.mWidthPixels / 2, getMeasuredHeight() – rectHeight – vertex, MyApplication.mWidthPixels, getMeasuredHeight() – rectHeight);
path.lineTo(MyApplication.mWidthPixels, getMeasuredHeight());

通过简单的几行代码就能描绘出一个带有贝塞尔曲线的矩形。其中moveTo方法为路径指定起点,如果不指定会默认从(0,0)开始。lineTo画一条直线。quadTo方法就是用来画贝塞尔曲线用的了,该方法需要传入四个参数,其实就是需要传入两个点的坐标。至于是那两个点这里就了解贝塞尔曲线的原理,因为本人很懒就不画图口头描述一下,需要详细了解的可以自行百度。

连接屏幕上两个点会形成一条直线,但是如果在两点之外的某个区域再选择一个点将三点相连,只要这三个点不是都在一条直线上,那么用直线相连就会形成一个三角形,而用平滑的曲线相连的话,形成的图形我们就称之为贝塞尔曲线,因此组成最基础的贝塞尔曲线需要三个点,一个起点,一个终点,一个在起点和终点形成的直线之外的点,这里我姑且把它称为拐点。而我们在用Path进行绘画的时候,无论是画什么东西,其实起点都已经被默认为上次所画的终点,因此quadTo方法需要传入的两个点坐标分别是拐点和终点的坐标值。

看文字确实不容易理解,最简单的方法就是自己动手去画一个就很清楚了。

贝塞尔曲线的画法很简单,但是我们要怎么运用才是最重要的。
要用贝塞尔曲线画一个简单且炫酷的动画大致思路可以是这样:

在开发中我们经常会遇到需要弹出的窗口等情况,通常我们会使用一个位移+透明等等的补间动画来实现,但是在有了贝塞尔曲线之后我们可以在窗口弹出的同时为该窗口生成一个由贝塞尔曲线所描绘的轮廓形状,并且该形状最开始可以是一条直线或者是很小的弧度,之后随着窗口弹出的距离逐渐增加曲线的弧度。这样就完成了一个最简单的基于贝赛尔曲线的渐变动画。同时还可以根据具体的需求决定是否保留该弧度,如果需要则在动画完成之后再将弧度消失,这样会达到一个更好的视觉效果。

就以上述效果为例,遇到的技术难度最主要就是如何让弧度渐变。渐变的方法很多,可以直接使用ValueAnimator进行实现,只需要设置一个渐变时长(duration),弧度最小最大值就基本完成了。但是普通的属性动画每秒的数值变化量都是相同的,虽然贝塞尔曲线本身自带平滑过度效果,但是为了更好的显示效果需要让动画的实线具有一个变化的速度。

在Android 5.0之后Google为了配合自家的Material Design将动画过渡做的更加平滑,引入了很多变速属性,这里我们需要用到的也就是这个。通过setInterpolator(TimeInterpolator value)方法可以为某个动画设置变速器,并且几乎Android所有原声动画都支持该方法,例如补间动画,属性动画等等。最基础也是最常用的加速器有以下两个:

AccelerateInterpolator
加速器,最开始速度最慢,往后逐渐增加,最终加到最大值。
DecelerateInterpolator
减速器,最开始速度最快,往后逐渐减小,最终见到最小值。

这些做好之后再配合一些布局嵌套以及相应的补间动画就能做出比较炫酷且圆润的过度效果了,以下是部分核心代码:

public class BezierCurve extends View {
private final Paint mPaint;
private float rectHeight = 0;
private float vertex = 0;
private int maxVertex;
private int maxRectHeight;
public BezierCurve(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
maxVertex = PublicUtils.dip2px(80);
}
public void setMaxRectHeight(int height) {
maxRectHeight = height;
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Path path = new Path();
path.moveTo(0, getMeasuredHeight());
path.lineTo(0, getMeasuredHeight() – rectHeight);
path.quadTo(MyApplication.mWidthPixels / 2, getMeasuredHeight() – rectHeight – vertex, MyApplication.mWidthPixels, getMeasuredHeight() – rectHeight);
path.lineTo(MyApplication.mWidthPixels, getMeasuredHeight());
canvas.drawPath(path, mPaint);
}
public void open() {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setIntValues(0, maxVertex);
valueAnimator.setDuration(500);
valueAnimator.setInterpolator(new AccelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
vertex = (int) animation.getAnimatedValue();
rectHeight = vertex / maxVertex * maxRectHeight;
invalidate();
if (vertex == maxVertex) {
stop();
}
}
});
valueAnimator.start();
}
private void stop() {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setIntValues(maxVertex, 0);
valueAnimator.setDuration(250);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
vertex = (int) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.start();
}
public void close() {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setIntValues(maxRectHeight, 0);
valueAnimator.setDuration(500);
// valueAnimator.setInterpolator(new AccelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
rectHeight = (int) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.start();
}
}