您的位置:首页 > 移动开发 > Android开发

[Android]贝塞尔曲线应用及QQ气泡拖动原理实践

2016-01-06 19:59 806 查看

贝塞尔曲线应用及QQ气泡拖动原理实践

绘线API介绍

moveTo

控制Path的起点位置

lineTo

从起点连接到某点位置

quadTo

绘制贝塞尔曲线

cubicTo

同样是绘制贝塞尔曲线,多一个起点坐标参数,比起quadTo省去了一个moveTo步骤

arcTo

截取圆弧的一部分角度

这部分读者可以直接参考别人的实例讲解

我在这儿主要介绍贝塞尔曲线的应用,虽然有很多人已经解释过什么是贝塞尔曲线,但大部分都没有说清楚公式转化的那部分,所以我还是在这里记录下来。

首先是一阶的贝塞尔曲线:



其对应的公式



然后是二阶的贝塞尔曲线:



其对应的公式



二阶函数公式对应的曲线为什么是这样的呢,这里可以把公式拆开转化,依次如下:





和一阶的公式联系起来看,继续转化:

用B0和B1代替和一阶公式对应的部分



B0和B1分别是P0到P1和P1到P2的1阶贝塞尔曲线。而2阶贝塞尔曲线B就是B0到B1的1阶贝塞尔曲线。





看着最后的这个公式,回去看看那个二阶的动态图,有木有瞬间明白了!

那么如果想问还有三阶、四阶什么的呵呵嗒,我这里转两个图读者自己去转化吧=。=#

三阶:



四阶:



QQ 气泡拖动原理解析及相关Demo

很早就看了QQ消息气泡的功能,也有很多人做过相关实现,类似于下面的,都是用贝塞尔曲线实现的:



于是自己也去实现了一个简单的Demo,主要也是想学习实现的原理,写这篇博客即是想记录,也是想分享。上面的Demo最后是用帧图片去做的,我就不去找图片了,改成了回弹的效果。

效果是这样的:



代码不多,直接铺出来:

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
* Created by Yellow5A5 on 15/12/18.
*/
public class ElasticRoundView extends View {

//变化因子,用于设置拖动距离与半径变化的关系
private final int CHANGE_FACTOR = 8;

private int density;
private int displayWidth;
private int displayHeight;

//中心坐标
private float mCenterX;
private float mCenterY;
//移动的圆中心坐标
private float mMovingX;
private float mMovingY;

//初始半径记录
private float mStartRadius;
//中心的圆半径
private float mCenterRadius;
//移动的圆半径
private float mMovingRadius;
//限制拖动范围
private float mLimit;
//标记最后ACTION_UP的坐标
private float mEndX, mEndY;

private Path mPath;
private Paint mPaint;
private ValueAnimator animator;

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

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

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

private void init() {
density = (int) getResources().getDisplayMetrics().density;
displayWidth = getResources().getDisplayMetrics().widthPixels;
displayHeight = getResources().getDisplayMetrics().heightPixels;

mCenterX = displayWidth / 2;
mCenterY = displayHeight / 2;
mCenterRadius = density * 25;
mStartRadius = mCenterRadius;

mPath = new Path();
mPaint = new Paint();
mPaint.setColor(Color.parseColor("#ff5777"));
mPaint.setAntiAlias(true);//去除锯齿
mPaint.setStyle(Paint.Style.FILL);

mMovingX = mCenterX;
mMovingY = mCenterY;
mMovingRadius = mCenterRadius;
mLimit = 7 * mCenterRadius;
initAnim();
updatePath();
}

//设置回归动画
private void initAnim() {
animator = ValueAnimator.ofFloat(1f, 0f).setDuration(1500);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMovingX = mCenterX + ((mEndX - mCenterX) * (float) animation.getAnimatedValue());
mMovingY = mCenterY + ((mEndY - mCenterY) * (float) animation.getAnimatedValue());
mCenterRadius = mStartRadius - vectorToPoint(mCenterX, mCenterY, mMovingX, mMovingY) / CHANGE_FACTOR;
mMovingRadius = mStartRadius - mCenterRadius;
updatePath();
invalidate();
}
});
}

//更新路径参数
private void updatePath() {
if (mMovingY == mCenterY || mMovingX == mCenterX)
return;
double corners = Math.atan((mMovingY - mCenterY) / (mMovingX - mCenterX));

float offsetX1 = (float) (mCenterRadius * Math.sin(corners));
float offsetY1 = (float) (mCenterRadius * Math.cos(corners));

float offsetX2 = (float) (mMovingRadius * Math.sin(corners));
float offsetY2 = (float) (mMovingRadius * Math.cos(corners));

float x1 = mCenterX - offsetX1;
float y1 = mCenterY + offsetY1;

float x2 = mMovingX - offsetX2;
float y2 = mMovingY + offsetY2;

float x3 = mMovingX + offsetX2;
float y3 = mMovingY - offsetY2;

float x4 = mCenterX + offsetX1;
float y4 = mCenterY - offsetY1;

float midpointX = (mCenterX + mMovingX) / 2;
float midpointY = (mCenterY + mMovingY) / 2;

mPath.reset();
mPath.moveTo(x1, y1);
mPath.quadTo(midpointX, midpointY, x2, y2);
mPath.lineTo(x3, y3);
mPath.quadTo(midpointX, midpointY, x4, y4);
mPath.lineTo(x1, y1);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
int eventAction = event.getAction();
int x = (int) event.getX();
int y = (int) event.getY();
float temp = 0;
switch (eventAction) {
case MotionEvent.ACTION_DOWN:
if (x < mCenterX - mCenterRadius || x > mCenterX + mCenterRadius || y < mCenterY - mCenterRadius || y > mCenterY + mCenterRadius) {
return false;
}
case MotionEvent.ACTION_MOVE:
mMovingX = x;
mMovingY = y;
temp = vectorToPoint(mCenterX, mCenterY, mMovingX, mMovingY);
if (temp > mLimit) {//限制拖动长度。
float multiple = mLimit / temp;
mMovingX = (mMovingX - mCenterX) * multiple + mCenterX;
mMovingY = (mMovingY - mCenterY) * multiple + mCenterY;
temp = mLimit;
}
mCenterRadius = mStartRadius - temp / CHANGE_FACTOR;
mMovingRadius = mStartRadius - mCenterRadius;
updatePath();
invalidate();
return true;
case MotionEvent.ACTION_UP:
//限制拖动长度。
mEndX = mMovingX;
mEndY = mMovingY;
animator.start();
}
return true;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mCenterX, mCenterY, mCenterRadius, mPaint);
canvas.drawCircle(mMovingX, mMovingY, mMovingRadius, mPaint);
canvas.drawPath(mPath, mPaint);
}

/**
* 计算两点之间的距离
* @return 两点之间的距离
*/
private float vectorToPoint(float X1, float Y1, float X2, float Y2) {
return (float) Math.sqrt(Math.pow(Math.abs(X2 - X1), 2) + Math.pow(Math.abs(Y2 - Y1), 2));
}
}


这就是整个类了,复制即可用。

讲解部分:

首先看看onDraw的代码:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mCenterX, mCenterY, mCenterRadius, mPaint);
canvas.drawCircle(mMovingX, mMovingY, mMovingRadius, mPaint);
canvas.drawPath(mPath, mPaint);
}


首先绘制了两个圆,分别是拖动的圆和中心的圆。然后再绘制mPath。绘制mPath事实上是填充模式去绘制两个圆连接的部分,通过6个点的坐标绘制2条直线和2条贝塞尔曲线围成的。

下面看看mPath的曲线绘制部分:

//更新路径参数
private void updatePath() {
if (mMovingY == mCenterY || mMovingX == mCenterX)
return;
double corners = Math.atan((mMovingY - mCenterY) / (mMovingX - mCenterX));

float offsetX1 = (float) (mCenterRadius * Math.sin(corners));
float offsetY1 = (float) (mCenterRadius * Math.cos(corners));

float offsetX2 = (float) (mMovingRadius * Math.sin(corners));
float offsetY2 = (float) (mMovingRadius * Math.cos(corners));

float x1 = mCenterX - offsetX1;
float y1 = mCenterY + offsetY1;

float x2 = mMovingX - offsetX2;
float y2 = mMovingY + offsetY2;

float x3 = mMovingX + offsetX2;
float y3 = mMovingY - offsetY2;

float x4 = mCenterX + offsetX1;
float y4 = mCenterY - offsetY1;

float midpointX = (mCenterX + mMovingX) / 2;
float midpointY = (mCenterY + mMovingY) / 2;

mPath.reset();
mPath.moveTo(x1, y1);
mPath.quadTo(midpointX, midpointY, x2, y2);
mPath.lineTo(x3, y3);
mPath.quadTo(midpointX, midpointY, x4, y4);
mPath.lineTo(x1, y1);
}


先通过角度计算,获得两个园中心的坐标角度,这个需要在图上画一画比较清晰,然后通过这个角度去获得四个角上的点坐标,然后再去取连接两圆圆心的线的中点位置midpoint。用这个midpoint来作为贝塞尔曲线坐标的控制点,进而开始绘制。

另外我设置了一个CHANGE_FACTOR值,通过改变这个值来设置拖动距离和半径变化的关系。

接下来我看看touchEvent里面ACTION_MOVE的代码:

case MotionEvent.ACTION_MOVE:
mMovingX = x;
mMovingY = y;
temp = vectorToPoint(mCenterX, mCenterY, mMovingX, mMovingY);
if (temp > mLimit) {//限制拖动长度。
float multiple = mLimit / temp;
mMovingX = (mMovingX - mCenterX) * multiple + mCenterX;
mMovingY = (mMovingY - mCenterY) * multiple + mCenterY;
temp = mLimit;
}
mCenterRadius = mStartRadius - temp / CHANGE_FACTOR;
mMovingRadius = mStartRadius - mCenterRadius;
updatePath();
invalidate();
return true;


这部分的逻辑负责更新移动中的圆的坐标及两圆半径。这部分坐标的更新先需要通过vectorToPoint方法来计算两圆距离,并是否超出限制,进而符合逻辑的更新绘制。

根据这样的效果实现,明白了实现原理,对于那些看起来很像特别酷炫的效果,像下面这个Demo,你是不是也应该有了头绪?



文章到此结束,谢谢阅读!

附参考文章:

http://segmentfault.com/a/1190000000721127

http://blog.csdn.net/zhongkejingwang/article/details/38556891

http://www.cnblogs.com/tianzhijiexian/p/4301113.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息