欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

贝塞尔曲线的艺术---弹性效果实现

程序员文章站 2022-03-20 10:14:50
...

前言

前段时间需要修改系统进度条的视觉效果,并查看了SeekBar的style样式,发现里面用到了以前没留意的一个图形技术-贝塞尔曲线。现在刚好项目不是特别忙就来研究一下。

贝塞尔曲线有一阶贝塞尔曲线,二阶贝塞尔曲线,三阶贝塞尔曲线...N阶贝塞尔曲线。一阶贝塞尔曲线就是一条直线,这里不做详解,这里主要用两个样例来看看二阶和三阶贝塞尔曲线可以做什么。


贝塞尔曲线理论知识


二阶贝塞尔曲线由三个点来控制,起点,终点和控制点。形状是一个抛物线的效果。

公式如下:

贝塞尔曲线的艺术---弹性效果实现

公式可能看起来比较晕,详细解读可以看看http://www.cnblogs.com/wjtaigwh/p/6647114.html这篇文章的解读,从图像中可以看出来二阶贝塞尔曲线有起点,终点和一个控制点来决定图形形状。

P0为起点,P1位控制点,P2为终点,t从0到1变化,比如当前t = 0.4,那么我们从P0出发,找出P0P1线段长度4/10的位置点P3,并且找出P1P2线段长度4/10的位置点P4,接着我们从P3出发,找出P3P4线段长度4/10的位置点,这个点就是t数值时刻,二阶贝塞尔曲线上点。

有兴趣的同学可以从这个图形出发推导出公式看看是否与上面给出的公式一致。


三阶贝塞尔曲线则有四个点来控制,起点,终点和两个控制点。

公式如下:

贝塞尔曲线的艺术---弹性效果实现


其实三阶以上图形的绘制很二阶曲线很相似。

P0为起点,P3为终点,P1,P2为控制点。t依然为0.4。第一步,从P0出发找出P0P1线段长度4/10的位置点P4,从P1出发找出P1P2线段长度4/10的位置点P5,从P2出发找出P2P3线段长度4/10的位置点P6,现在产生了三个新的点P4,P5,P6。第二步,从P4出发找出P4P5线段长度4/10的位置点P7,从P5出发找出P5P6线段长度4/10的位置点P8,现在多出了两个新的点P7,P8。第四步,从P7出发找出P7P9线段长度4/10的位置点P9。好了,P9就是t数值时贝塞尔曲线上的点。

二阶贝塞尔曲线要经历2步,三阶贝塞尔曲线要经过3步计算,意思类推,四阶贝塞尔曲线就要经过四步计算了。是不是很简单呢?


下面我们来写一下根据t和n个点来计算t时刻的坐标吧。n个点绘制出来的就是n-1阶贝塞尔曲线了。

public float calculate(float t, float... values){
        for(int i= values.length-1;i>0;i--)
            for (int j =0;j<i;j++){
                values[j] = values[j] + values[j+1];
            }
        return values[0];
}

这里我们把x分量和y分量分开来计算就可以得到t时刻的点坐标了,那么我们可以把0-1划分的很细,比如10000份,那么就可以得到10000个贝塞尔曲线上的点,再把相邻的点连接起来就可以看到一条平滑的曲线了,这就是贝塞尔曲线,不得不感叹数学的魅力,也致敬一下贝塞尔这个数学家吧=_=。


贝塞尔曲线能干什么

在我们的现实生活中其实有很多曲线的体现,比如海中波浪的形状,各种电子设备棱角的设计等等,那么我们就可以用贝塞尔曲线来模拟出这些曲线,这里会有一个特殊的形状,圆形。二阶贝塞尔曲线和三阶贝塞尔曲线都能模拟出圆形。先看看三阶贝塞尔曲线怎么模拟圆形吧。后面会用到

这里其实是有一篇论文来着,地址忘了读者自己去找吧,哈哈,反正知道一个0.552284749831这个数值就好了,我们把圆形分为四份来绘制,比如我们制定圆心为(100,100),绘制一个半径为100的圆形,那么我们就绘制(100,0)到(200,100)这1/4的圆弧吧,找到从(100,0)到(200,0)这条线段长度100 * 0.552284749831位置点,这里就指定为(155.22, 0)吧,同理还有一个点(200,100-55.22)这个点,这两个点就是1/4圆弧曲线的控制点,起点为(100,0)和(200,100)。这四个点绘制出来的曲线就十分接近1/4圆弧了。记住0.5522...这个数值,哈哈。


贝塞尔曲线早android中最大的作用就是制作动画效果了吧,不多说了,看了我做的两个小实例就明白啦,当然我也是接触不就,所以模拟的效果也没那么逼真,讲究这看吧。


贝塞尔曲线实例--圆形到心形的动画效果

先上代码吧
public class HeartView extends View{
	
	private static final float RATIO = 0.5522f;
	private static final int INIT_X = 400;
	private static final int INIT_Y = 400;
	private static final int RADIUS = 200;
	private static final int DURATION = 500;
	
	private PointF mCenterPoint = new PointF();
	
	private PointF mLeftPoint = new PointF();
	private PointF mTopPoint = new PointF();
	private PointF mRightPoint = new PointF();
	private PointF mBottomPoint = new PointF();
	
	private PointF mLC1Point = new PointF();
	private PointF mLC2Point = new PointF();
	private PointF mTC1Point = new PointF();
	private PointF mTC2Point = new PointF();
	private PointF mRC1Point = new PointF();
	private PointF mRC2Point = new PointF();
	private PointF mBC1Point = new PointF();
	private PointF mBC2Point = new PointF();
	
	private Path mPath = new Path();
	private Paint mPaint = new Paint();
	
	private ValueAnimator mAnimator;

	public HeartView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init();
	}

	public HeartView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public HeartView(Context context) {
		this(context, null);
	}

	private void init(){
		mPaint.setStyle(Style.FILL_AND_STROKE);
		mPaint.setStrokeWidth(5);
		mPaint.setColor(Color.RED);
		mCenterPoint.set(INIT_X, INIT_Y);
		computePoint();
		mAnimator = ValueAnimator.ofInt(0, DURATION);
		mAnimator.addUpdateListener(new AnimatorUpdateListener() {
			
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				float ratio = ((int)animation.getAnimatedValue()) / (DURATION * 1.0f);
				computePoint();
				mLeftPoint.set(mCenterPoint.x - RADIUS, mCenterPoint.y - RADIUS * RATIO * 0.2f * ratio);
				mRightPoint.set(mCenterPoint.x + RADIUS, mCenterPoint.y - RADIUS * RATIO * 0.2f * ratio);
				mTopPoint.set(mCenterPoint.x, mCenterPoint.y - RADIUS + RADIUS * 0.50f * ratio);
				mBC1Point.set(mBottomPoint.x  - RADIUS * RATIO , mBottomPoint.y - (RADIUS * RATIO * 0.80f * ratio));
				mBC2Point.set(mLeftPoint.x, mLeftPoint.y + RADIUS * RATIO + RATIO * RADIUS * 0.00f * ratio);
				mRC1Point.set(mRightPoint.x,  mRightPoint.y + RADIUS * RATIO + RATIO * RADIUS * 0.00f * ratio);
				mRC2Point.set(mBottomPoint.x + RADIUS * RATIO, mBottomPoint.y - (RADIUS * RATIO * 0.80f * ratio));
				invalidate();
			}
		});
		mAnimator.setDuration(DURATION);
		mAnimator.setStartDelay(1000);
		mAnimator.start();
	}
	
	@Override
	protected void onFinishInflate() {
		super.onFinishInflate();
	}
	
	private void computePoint(){
		mLeftPoint.set(mCenterPoint.x - RADIUS, mCenterPoint.y);
		mTopPoint.set(mCenterPoint.x, mCenterPoint.y - RADIUS);
		mRightPoint.set(mCenterPoint.x + RADIUS, mCenterPoint.y);
		mBottomPoint.set(mCenterPoint.x, mCenterPoint.y + RADIUS);
		
		mLC1Point.set(mLeftPoint.x, mLeftPoint.y - RADIUS * RATIO);
		mLC2Point.set(mTopPoint.x - RADIUS * RATIO, mTopPoint.y);
		mTC1Point.set(mTopPoint.x + RADIUS * RATIO, mTopPoint.y);
		mTC2Point.set(mRightPoint.x, mRightPoint.y - RADIUS * RATIO);
		mRC1Point.set(mRightPoint.x,  mRightPoint.y + RADIUS * RATIO);
		mRC2Point.set(mBottomPoint.x + RADIUS * RATIO, mBottomPoint.y);
		mBC1Point.set(mBottomPoint.x - RADIUS * RATIO, mBottomPoint.y);
		mBC2Point.set(mLeftPoint.x, mLeftPoint.y + RADIUS * RATIO);
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		mPath.reset();
		mPath.moveTo(mLeftPoint.x, mLeftPoint.y);
		mPath.cubicTo(mLC1Point.x, mLC1Point.y, mLC2Point.x, mLC2Point.y, mTopPoint.x, mTopPoint.y);
		mPath.cubicTo(mTC1Point.x, mTC1Point.y, mTC2Point.x, mTC2Point.y, mRightPoint.x, mRightPoint.y);
		mPath.cubicTo(mRC1Point.x, mRC1Point.y, mRC2Point.x, mRC2Point.y, mBottomPoint.x, mBottomPoint.y);
		mPath.cubicTo(mBC1Point.x, mBC1Point.y, mBC2Point.x, mBC2Point.y, mLeftPoint.x, mLeftPoint.y);
		canvas.drawPath(mPath, mPaint);
		super.onDraw(canvas);
	}
}



这里可以看到android其实已经提供了贝塞尔曲线的接口,包括1阶到三阶的绘制接口,其实一节就是绘制直线的API,这里就不多讲,先看看二阶贝塞尔曲线的两个api:






public void quadTo(float x1, float y1, float x2, float y2);
public void rQuadTo(float dx1, float dy1, float dx2, float dy2);
去看方法注释,你就会明白第一个方法x1,x2是起点,x2是终点,那么起点怎么制定呢?我们用Path.move(x,y)制定起点(x,y)。第二个方法中dx1,就是控制点x坐标相对于
起始点x的偏移量,其他三个参数都可以以此类推。

我们要模拟圆形变心形,首先需要绘制出圆形的路径。
computePoint()这个方法就是根据圆心绘制出4个1/4圆路径组合成一个完整的圆形,这里用的是三次贝塞尔曲线,当然二次贝塞尔曲线也是可以的,读者可以自己去研究下。
这里由4个点把圆形分为四个圆弧A(200,400),B(400,200),C(600,400),D(400,600)
接下来就是怎么变成心形,其实心形和圆都是对称的,我们只要绘制左边的就知道右边图形怎么变换了。
第一步,AB圆弧的变化。圆心的效果有我们由我们自己决定,这里我将A点的上移一段距离,B点下移一段距离,圆弧就变成了我们想要的形状,距离效果读者可以在调节,包括控制点也是可以移动的,我这里就不做移动了。
第二步,AD圆弧的变化。第一步中A点已经做了移动,这里D点保持原位置,这里要做的是靠近A的控制点下移,靠近D的控制点下移。变化的结果就是下图中左下角的图形了。
第三步,右边的图形参照左边的做对称变换,这里不做叙述了。

点的变化看下图

贝塞尔曲线的艺术---弹性效果实现

是不是觉得很简单,是不是觉得打开动画效果的大门了,哈哈。


贝塞尔曲线实例--模拟皮球的撞击运动

直接上代码
public class FallBall extends View {
	
	public static final String TAG = "FallBall";
	
	private static final float RATIO = 0.5522f;
	private static final int RADIUS = 200;
	private static final int INIT_CENTER_X = 600;
	private static final int INIT_CENTER_Y = 200;
	private static final int HEIGHT = 1000;
	
	private static final int DURATION = 2000;
	private static final int DURATION2 = 300;
	private float mAcceSpeed;
	
	private int mCurrentCenterX = INIT_CENTER_X;
	private int mCurrentCenterY = INIT_CENTER_Y;
	private Point mCenterPoint = new Point(mCurrentCenterX, mCurrentCenterY);
	private Point mLastPoint = new Point();
	
	private Point mLeftStartPoint = new Point();
	private Point mTopStartPoint = new Point();
	private Point mRightStartPoint = new Point();
	private Point mBottomStartPoint = new Point();
	
	private Point mLeftControlPoint_1 = new Point();
	private Point mTopControlPoint_1 = new Point();
	private Point mRightControlPoint_1 = new Point();
	private Point mBottomControlPoint_1 = new Point();
	
	private Point mLeftControlPoint_2 = new Point();
	private Point mTopControlPoint_2 = new Point();
	private Point mRightControlPoint_2 = new Point();
	private Point mBottomControlPoint_2 = new Point();
	
	private Paint mPaint = new Paint();
	private Paint mFloorPaint = new Paint();
	
	private Path mPath = new Path();
	
	private ValueAnimator mValueAnimator;
	private ValueAnimator mValueAnimator2;
	private ValueAnimator mValueAnimator3;
	private ValueAnimator mValueAnimator4;
	private AnimatorSet mAnimatorSet;

	public FallBall(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init();
	}

	public FallBall(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public FallBall(Context context) {
		this(context, null);
	}
	
	private void init(){
		mPaint.setAntiAlias(true);
		mPaint.setColor(Color.RED);
		mFloorPaint.setAntiAlias(true);
		mFloorPaint.setColor(Color.GRAY);
		mFloorPaint.setStrokeWidth(340);
		mAcceSpeed = HEIGHT *2.0f / (DURATION * DURATION);
		mAnimatorSet = new AnimatorSet();
	}
	
	@Override
	protected void onFinishInflate() {
		super.onFinishInflate();
		mValueAnimator = ValueAnimator.ofInt(0,DURATION);
		mValueAnimator.setDuration(DURATION);
		mValueAnimator.addUpdateListener(new AnimatorUpdateListener() {
			
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				int value = (int) animation.getAnimatedValue();
				Log.v(TAG, "time :" +  value);
				mCenterPoint.y = (int) (INIT_CENTER_Y + 0.5f * mAcceSpeed * value * value);
				computePoint(mCenterPoint);
				invalidate();
			}
		});
		mValueAnimator.addListener(new AnimatorListener() {
			
			@Override
			public void onAnimationStart(Animator animation) {
			}
			
			@Override
			public void onAnimationRepeat(Animator animation) {
				
			}
			
			@Override
			public void onAnimationEnd(Animator animation) {
				mLastPoint.y = mLeftStartPoint.y;
				mLastPoint.x = mLeftStartPoint.x;
			}
			
			@Override
			public void onAnimationCancel(Animator animation) {
				
			}
		});
		mValueAnimator2 = ValueAnimator.ofInt(0, DURATION2);
		mValueAnimator2.addUpdateListener(new AnimatorUpdateListener() {
			
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				int value = (int)animation.getAnimatedValue();
				int h = RADIUS * 2 + HEIGHT - mLastPoint.y;
				float acceSpeed = (2.0f * h) / (DURATION2 * DURATION2);
				mCenterPoint.y = (int) (RADIUS * 2 + HEIGHT - RADIUS + acceSpeed * DURATION2 * value - 0.5f * acceSpeed * value * value);
				computePoint(mCenterPoint);
				float acceSpeed2 = (float) ((RADIUS / Math.sqrt(2)) * RATIO * 2.0f / (DURATION2 * DURATION2));
				float s = (acceSpeed2 * DURATION2 * value - 0.5f * acceSpeed2 * value * value);
				mBottomControlPoint_1.x = (int) (mBottomStartPoint.x - RADIUS / Math.sqrt(2) * RATIO  + s);
				mBottomControlPoint_1.y = (int) (mBottomStartPoint.y + RADIUS / Math.sqrt(2) * RATIO - s);
				mBottomControlPoint_2.x = (int) (mLeftStartPoint.x + RADIUS / Math.sqrt(2) * RATIO  - s);
				mBottomControlPoint_2.y = (int) (mLeftStartPoint.y + RADIUS / Math.sqrt(2) * RATIO  - s);
					
				invalidate();
			}
		});
		mValueAnimator2.setDuration(DURATION2);
		mValueAnimator3 = ValueAnimator.ofInt(DURATION2);
		mValueAnimator3.setDuration(DURATION2);
		mValueAnimator3.addUpdateListener(new AnimatorUpdateListener() {
			
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				int value = (int)animation.getAnimatedValue();
				int h = RADIUS * 2 + HEIGHT - mLastPoint.y;
				float acceSpeed = (2.0f * h) / (DURATION2 * DURATION2);
				mCenterPoint.y = (int) (RADIUS * 2 + HEIGHT - RADIUS + h - 0.5f * acceSpeed * value * value);
				computePoint(mCenterPoint);
				float acceSpeed2 = (float) ((RADIUS / Math.sqrt(2)) * RATIO * 2.0f / (DURATION2 * DURATION2));
				float s = 0.5f * acceSpeed2 * value * value;
				mBottomControlPoint_1.x = (int) (mBottomStartPoint.x - s);
				mBottomControlPoint_1.y = (int) (mBottomStartPoint.y + s);
				mBottomControlPoint_2.x = (int) (mLeftStartPoint.x + s);
				mBottomControlPoint_2.y = (int) (mLeftStartPoint.y + s);	
				invalidate();
			}
		});
		mValueAnimator4 = ValueAnimator.ofInt(DURATION);
		mValueAnimator4.setDuration(DURATION);
		mValueAnimator4.addUpdateListener(new AnimatorUpdateListener() {
			
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				int value = (int) animation.getAnimatedValue();
				Log.v(TAG, "time :" +  value);
				mCenterPoint.y = (int) ( INIT_CENTER_Y + HEIGHT - (mAcceSpeed * DURATION * value - 0.5f * mAcceSpeed * value * value));
				computePoint(mCenterPoint);
				invalidate();
			}
		});
		mAnimatorSet.playSequentially(new Animator[]{mValueAnimator, mValueAnimator2, mValueAnimator3, mValueAnimator4});
		mAnimatorSet.start();
		mAnimatorSet.addListener(new AnimatorListener() {
			
			@Override
			public void onAnimationStart(Animator animation) {
				
			}
			
			@Override
			public void onAnimationRepeat(Animator animation) {
				
			}
			
			@Override
			public void onAnimationEnd(Animator animation) {
				mAnimatorSet.start();
			}
			
			@Override
			public void onAnimationCancel(Animator animation) {
				
			}
		});
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		mPath.reset();
		mPath.moveTo(mLeftStartPoint.x, mLeftStartPoint.y);
		mPath.cubicTo(mLeftControlPoint_1.x, mLeftControlPoint_1.y, mLeftControlPoint_2.x, mLeftControlPoint_2.y, mTopStartPoint.x, mTopStartPoint.y);
		mPath.cubicTo(mTopControlPoint_1.x, mTopControlPoint_1.y, mTopControlPoint_2.x, mTopControlPoint_2.y, mRightStartPoint.x, mRightStartPoint.y);
		mPath.cubicTo(mRightControlPoint_1.x, mRightControlPoint_1.y, mRightControlPoint_2.x, mRightControlPoint_2.y, mBottomStartPoint.x, mBottomStartPoint.y);
		mPath.cubicTo(mBottomControlPoint_1.x, mBottomControlPoint_1.y, mBottomControlPoint_2.x, mBottomControlPoint_2.y, mLeftStartPoint.x, mLeftStartPoint.y);
		canvas.drawPath(mPath, mPaint);
		
		canvas.drawLine(0, RADIUS * 2 + HEIGHT + 170, 1080, RADIUS * 2 + HEIGHT + 170, mFloorPaint);
		super.onDraw(canvas);
	}
	
	
	private void computePoint(Point centerPoint){
		mLeftStartPoint.x = (int) (centerPoint.x - RADIUS/ Math.sqrt(2));
		mLeftStartPoint.y = (int) (centerPoint.y + RADIUS/ Math.sqrt(2));
		mTopStartPoint.x = (int) (centerPoint.x - RADIUS/ Math.sqrt(2));
		mTopStartPoint.y = (int) (centerPoint.y - RADIUS/ Math.sqrt(2));
		mRightStartPoint.x = (int) (centerPoint.x + RADIUS/ Math.sqrt(2));
		mRightStartPoint.y = (int) (centerPoint.y - RADIUS/ Math.sqrt(2));
		mBottomStartPoint.x = (int) (centerPoint.x + RADIUS/ Math.sqrt(2));
		mBottomStartPoint.y = (int) (centerPoint.y + RADIUS/ Math.sqrt(2));
		
		mLeftControlPoint_1.x = (int) (mLeftStartPoint.x - RADIUS / Math.sqrt(2) * RATIO);
		mLeftControlPoint_1.y = (int) (mLeftStartPoint.y - RADIUS / Math.sqrt(2) * RATIO);
		mTopControlPoint_1.x = (int) (mTopStartPoint.x + RADIUS / Math.sqrt(2) * RATIO);
		mTopControlPoint_1.y = (int) (mTopStartPoint.y - RADIUS / Math.sqrt(2) * RATIO);
		mRightControlPoint_1.x = (int) (mRightStartPoint.x + RADIUS / Math.sqrt(2) * RATIO);
		mRightControlPoint_1.y = (int) (mRightStartPoint.y + RADIUS / Math.sqrt(2) * RATIO);
		mBottomControlPoint_1.x = (int) (mBottomStartPoint.x - RADIUS / Math.sqrt(2) * RATIO);
		mBottomControlPoint_1.y = (int) (mBottomStartPoint.y + RADIUS / Math.sqrt(2) * RATIO);
		
		mLeftControlPoint_2.x = (int) (mTopStartPoint.x - RADIUS / Math.sqrt(2) * RATIO);
		mLeftControlPoint_2.y = (int) (mTopStartPoint.y + RADIUS / Math.sqrt(2) * RATIO);
		mTopControlPoint_2.x = (int) (mRightStartPoint.x - RADIUS / Math.sqrt(2) * RATIO);
		mTopControlPoint_2.y = (int) (mRightStartPoint.y - RADIUS / Math.sqrt(2) * RATIO);
		mRightControlPoint_2.x = (int) (mBottomStartPoint.x + RADIUS / Math.sqrt(2) * RATIO);
		mRightControlPoint_2.y = (int) (mBottomStartPoint.y - RADIUS / Math.sqrt(2) * RATIO);
		mBottomControlPoint_2.x = (int) (mLeftStartPoint.x + RADIUS / Math.sqrt(2) * RATIO);
		mBottomControlPoint_2.y = (int) (mLeftStartPoint.y + RADIUS / Math.sqrt(2) * RATIO);
	}
}

这个例子其实跟上一个例子很相似,也是把圆形划分为四份,这里贝塞尔曲线主要用来模拟撞击时刻的运行轨迹。
这里划分跟上个例子稍微不同,从computePoint()方法可以看出是以类X的形状来划分。那么进行碰撞的刚好就是底部的1/4圆弧。
怎么模拟这个运行呢,想象一下,是不是底部不断凹陷,最终圆弧变直线,这样简单了,只需要把两个控制点往两个点的方向移动,这样圆弧就不断变小,最终变化为一条直线。
整个过程过程中圆弧的起点和终点会向下做匀减速运动,控制点以起点和终点为参考点进行运动。
然后速度为0之后球形会弹起来,刚好是对称的匀加速运动。
我们也开看看动画中各个点的运动吧,图形总是比较好理解一点嘛。
贝塞尔曲线的艺术---弹性效果实现


贝塞尔曲线--射箭的动画



上代码
public class BowView extends View {
	
	private static final int STATUS_DRAGING = 1;
	private static final int STATUS_RELEASING = 2;
	private static final int STATUS_INIT = 0;
	
	private static final int DUARATION = 30;
	private static final int DUARATION2 = 500;
	
	private static final int INIT_X = 500;
	private static final int INIT_Y = 300;
	private static final int RADIUS = 400;
	private static final int BOW_C_HEIGHT = 300;
	private static final int BOW_STRING_HEIGHT_MAX = 200;
	private static final int DECORATE_RADIUS = 25;
	
	private static final int ARROW_LENGTH = 500;
	private static final int ARROW_MAX_DISTANCE = 3000;
	
	private static final int DECORATE_X_OFFSET = 60;
	private static final int DECORATE_Y_OFFSET = 70;
	
	private PointF mBowLeftPoint = new PointF();
	private PointF mBowRightPoint = new PointF();
	private PointF mBowControlPoint = new PointF();
	private PointF mBowStringCenterPoint = new PointF();
	private PointF mBowStringReleasePoint = new PointF();
	private PointF mArrawPoint = new PointF();
	
	private Path mPath = new Path();
	private Paint mBowPaint = new Paint();
	private Paint mBowStringPaint = new Paint();
	private Paint mDecoratePaint = new Paint();
	private Paint mArrowPaint = new Paint();
	
	private int mStatus = STATUS_INIT;
	
	private boolean isFlying = false;
	
	private ValueAnimator mBowStringAnimator;
	private ValueAnimator mArrowAnimator;

	public BowView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init();
	}

	public BowView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public BowView(Context context) {
		this(context, null);
	}
	
	private void init(){
		mBowPaint.setStrokeWidth(20);
		mBowPaint.setColor(Color.DKGRAY);
		mBowPaint.setStyle(Style.STROKE);
		mBowPaint.setAntiAlias(true);
		
		mBowStringPaint.setStrokeWidth(4);
		mBowStringPaint.setStyle(Style.STROKE);
		mBowStringPaint.setColor(Color.RED);
		mBowPaint.setAntiAlias(true);
		
		mDecoratePaint.setStyle(Style.FILL);
		mDecoratePaint.setColor(Color.BLACK);
		mDecoratePaint.setAntiAlias(true);
		
		mArrowPaint = new Paint();
		mArrowPaint.setStyle(Style.STROKE);
		mArrowPaint.setColor(Color.BLACK);
		mArrowPaint.setStrokeWidth(8);
		mArrowPaint.setAntiAlias(true);
		
		mBowLeftPoint.set(INIT_X - RADIUS, INIT_Y);
		mBowRightPoint.set(INIT_X + RADIUS,  INIT_Y);
		mBowControlPoint.set(INIT_X, INIT_Y + BOW_C_HEIGHT);
		mBowStringCenterPoint.set(INIT_X, INIT_Y);
		
		mBowStringAnimator = ValueAnimator.ofFloat(0, DUARATION);
		mBowStringAnimator.setDuration(DUARATION);
		mBowStringAnimator.addUpdateListener(new AnimatorUpdateListener() {
			
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				float value = (float) animation.getAnimatedValue();
				mBowStringCenterPoint.set(INIT_X, mBowStringReleasePoint.y + value);
				computeDraging(mBowStringCenterPoint);
				invalidate();
			}
		});
		mBowStringAnimator.addListener(new AnimatorListener() {
			
			@Override
			public void onAnimationStart(Animator animation) {
				
			}
			
			@Override
			public void onAnimationRepeat(Animator animation) {
				
			}
			
			@Override
			public void onAnimationEnd(Animator animation) {
				mStatus = STATUS_INIT;
			}
			
			@Override
			public void onAnimationCancel(Animator animation) {
				
			}
		});
		mArrowAnimator = ValueAnimator.ofFloat(0, ARROW_MAX_DISTANCE);
		mArrowAnimator.setDuration(DUARATION2);
		mArrowAnimator.addUpdateListener(new AnimatorUpdateListener() {
			
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				float value = (float) animation.getAnimatedValue();
				mArrawPoint.set(INIT_X, mBowStringReleasePoint.y + value);
				invalidate();
			}
		});
		mArrowAnimator.addListener(new AnimatorListener() {
			
			@Override
			public void onAnimationStart(Animator animation) {
				isFlying = true;
			}
			
			@Override
			public void onAnimationRepeat(Animator animation) {
				
			}
			
			@Override
			public void onAnimationEnd(Animator animation) {
				isFlying = false;
			}
			
			@Override
			public void onAnimationCancel(Animator animation) {
				
			}
		});
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			PointF point = new PointF(event.getX(), event.getY());
			PointF originPoint = new PointF(INIT_X, INIT_Y);
			float distance = GraphUtil.distance4PointF(point, originPoint);
			if(distance < 200 && mStatus == STATUS_INIT){
				mStatus = STATUS_DRAGING;
			}
			break;
		case MotionEvent.ACTION_MOVE:
			if(event.getY() < INIT_Y && event.getY() > INIT_Y - BOW_STRING_HEIGHT_MAX){
				if(mStatus == STATUS_DRAGING){
					mBowStringCenterPoint.set(INIT_X, event.getY());
					mArrawPoint.set(mBowStringCenterPoint);
					computeDraging(mBowStringCenterPoint);
					invalidate();
				}
			}
			break;
		case MotionEvent.ACTION_UP:
			if(mBowControlPoint.y != INIT_Y && mStatus == STATUS_DRAGING){
				mStatus = STATUS_RELEASING;
				mBowStringReleasePoint.set(mBowStringCenterPoint.x, mBowStringCenterPoint.y);
				startBowStringAnimation();
				startArrowAnimation();
			}
			break;
		}
		return true;
	}
	
	private void computeDraging(PointF point){
		float ratio = (INIT_Y - point.y) / BOW_STRING_HEIGHT_MAX;
		mBowLeftPoint.set(INIT_X - RADIUS + DECORATE_X_OFFSET * ratio, INIT_Y - DECORATE_Y_OFFSET * ratio);
		mBowRightPoint.set(INIT_X + RADIUS - DECORATE_X_OFFSET * ratio, INIT_Y - DECORATE_Y_OFFSET * ratio);
	}
	
	private void startBowStringAnimation(){
		mBowStringAnimator.setFloatValues(0, INIT_Y - mBowStringReleasePoint.y);
		mBowStringAnimator.setInterpolator(new DecelerateInterpolator());
		mBowStringAnimator.start();
	}
	
	private void startArrowAnimation(){
		float ratio = (INIT_Y - mBowStringReleasePoint.y) / BOW_STRING_HEIGHT_MAX;
		float distance = ARROW_MAX_DISTANCE * ratio * ratio;
		mArrowAnimator.setFloatValues(0, distance);
		mArrowAnimator.setInterpolator(new DecelerateInterpolator());
		mArrowAnimator.start();
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		mPath.reset();
		mPath.moveTo(mBowLeftPoint.x, mBowLeftPoint.y);
		mPath.quadTo(mBowControlPoint.x, mBowControlPoint.y, mBowRightPoint.x, mBowRightPoint.y);
		canvas.drawPath(mPath, mBowPaint);
		canvas.drawLine(mBowStringCenterPoint.x, mBowStringCenterPoint.y, mBowLeftPoint.x, mBowLeftPoint.y, mBowStringPaint);
		canvas.drawLine(mBowStringCenterPoint.x, mBowStringCenterPoint.y, mBowRightPoint.x, mBowRightPoint.y, mBowStringPaint);
		canvas.drawCircle(mBowLeftPoint.x, mBowLeftPoint.y, DECORATE_RADIUS, mDecoratePaint);
		canvas.drawCircle(mBowRightPoint.x, mBowRightPoint.y, DECORATE_RADIUS, mDecoratePaint);
		if(mStatus == STATUS_DRAGING || isFlying){
			canvas.drawLine(mArrawPoint.x, mArrawPoint.y, mArrawPoint.x, mArrawPoint.y + ARROW_LENGTH, mArrowPaint);
			canvas.drawLine(mArrawPoint.x, mArrawPoint.y + ARROW_LENGTH -3, mArrawPoint.x - 20, mArrawPoint.y + ARROW_LENGTH - 40, mArrowPaint);
			canvas.drawLine(mArrawPoint.x, mArrawPoint.y + ARROW_LENGTH -3, mArrawPoint.x + 20, mArrawPoint.y + ARROW_LENGTH - 40, mArrowPaint);
		}		
	}
	
	@Override
	protected void onDetachedFromWindow() {
		if(mBowStringAnimator != null){
			mBowStringAnimator.cancel();
		}
		if(mArrowAnimator != null){
			mArrowAnimator.cancel();
		}
		super.onDetachedFromWindow();
	}
}

这里先介绍一下二阶贝塞尔曲线的API。

public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)

学习了上面三阶贝塞尔曲线api,这里的就很好理解了,x1,y1是控制点,x2,y2是终点,起点同样有Path.move(x,y)方法指定。
第二个方法很现实就是相对点了,这里就不做叙述了。

这个动画主要在有三个物体,弓体,弓弦和箭。
贝塞尔曲线主要用在了弓体的运动上,当然弓弦也是有曲线运动的,这里就不做这个效果了,读者有兴趣可以加上。
这里弓可以用二阶贝塞尔曲线绘制,两端两个点事起点和终点,下方一个控制点。
拉弓的时候我们只需要把起点和终点向上方移动,并且同时向内测移动。弦的终点向上方一定,这样就很容易模拟出了拉弓的动效。
箭的运动其实很简单,拉弓的时候箭的起点始终和弦的终点重合,箭终点根据起点向下移动箭长度就可以了。当我们放开弦的时候应该怎么运动呢,弓和弦肯定是恢复原位,而且是做减速运动,箭也是最减速运动,但是箭的运动距离要大很多,这个距离跟弓弦拉动的幅度成一个正比例关系。
拉弓的时候我们根据touch回调调整各个点的位置,放开弦的时候就开始我们的动画了,弓水平的时候其实箭会做一个抛物线运动,你应该想到了,也是一个贝塞尔曲线运动,这部分就由读者自己去补充吧,毕竟实践出真知嘛,哈哈。
最后看一下我们效果图和图中关键点的变化吧。
贝塞尔曲线的艺术---弹性效果实现
这一篇就讲到这里拉,贝塞尔曲线应用太多啦,所以我们可以用它模拟出现实中很多有意思的运动,读者自行去发掘吧,我们这里只是简单运动,很多运动要比这复杂多了,哈哈,当然想要模拟的更真实,动画中要控制的点就越多,要控制哪些点就需要用我们的眼睛来观察了,为了使得模拟起来更简单我们通常将一个运动分拆分几个部分,然后观察每个部分的特征点,掌控例如特征点,我们就能使它动起来。