贝塞尔曲线的艺术---弹性效果实现
前言
前段时间需要修改系统进度条的视觉效果,并查看了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的偏移量,其他三个参数都可以以此类推。
贝塞尔曲线实例--模拟皮球的撞击运动
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);
}
}
这个例子其实跟上一个例子很相似,也是把圆形划分为四份,这里贝塞尔曲线主要用来模拟撞击时刻的运行轨迹。
贝塞尔曲线--射箭的动画
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();
}
}
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)方法指定。
第二个方法很现实就是相对点了,这里就不做叙述了。
下一篇: 心形流水灯