贝塞尔曲线实践–拖拽气泡
贝塞尔曲线扫盲
贝塞尔曲线主要由于三个部分控制:起点,终点,中间的辅助控制点。在android自带的Path类中自带了方法,可以帮助我们实现贝塞尔曲线:
二阶贝塞尔
/**
* Add a quadratic bezier from the last point, approaching control point
* (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
* this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the control point on a quadratic curve
* @param y1 The y-coordinate of the control point on a quadratic curve
* @param x2 The x-coordinate of the end point on a quadratic curve
* @param y2 The y-coordinate of the end point on a quadratic curve
*/
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
native_quadTo(mNativePath, x1, y1, x2, y2);
}
quadTo()
方法从上一个点为起点开始绘制贝塞尔曲线,其中(x1,y1)为辅助控制点,(x2,y2)为终点。
Path mPath = new Path();
mPath.moveTo(x0,y0);
mPath.quadTo(x1,y1,x2,y2);
如调用以上代码,即绘制起点(x0,y0),终点(x2,y2),辅助控制点(x1,y1)的贝塞尔曲线。因此,通过不断改变这三个点的位置,我们可以绘制出各种各样的曲线
三阶贝塞尔
/**
* Add a cubic bezier from the last point, approaching control points
* (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
* made for this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the 1st control point on a cubic curve
* @param y1 The y-coordinate of the 1st control point on a cubic curve
* @param x2 The x-coordinate of the 2nd control point on a cubic curve
* @param y2 The y-coordinate of the 2nd control point on a cubic curve
* @param x3 The x-coordinate of the end point on a cubic curve
* @param y3 The y-coordinate of the end point on a cubic curve
*/
public void cubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
isSimplePath = false;
native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}
cubicTo()
方法从上一个点为起点开始绘制三阶贝塞尔曲线,其中(x1,y1),( x2, y2 )为辅助控制点,(x3,y3)为终点。
拖拽气泡实践
思路
拖拽气泡,其实 就是这样的几个状态
- 1.气泡静止状态 –气泡小球和数字
- 2.气泡相连 – 两个相连的气泡小球(带黏连效果),和数字
- 3.气泡分离 – 单个气泡小球的拖动
- 4.气泡消失 – 爆炸
我们需要在这几个状态分别,做出处理.
1.两个圆和一个角度
- 一个静止的圆O1
- 一个动的圆O2
- 互补角和内错角<@
根据圆的关系,和Anchor点,计算出角度<@,用来求贝塞尔曲线的ABCD点坐标.
2.两条贝塞尔二阶曲线
上半边: A—Ar—B, 以Ar为锚点,A和B重点的二阶曲线
下半边: C—Ar—D,以Ar为锚点,C和D重点的二阶曲线
具体坐标的求法,在上图中可见.
根据贝塞尔曲线的要求:
起点,终点,中间的辅助控制点。用在android自带的Path类中自带了方法.
举个例子,就可以这么做
上边线:
//画上半弧 B-->A---------------
//A点静坐标计算
float mBubStillAX = mBubStillCenter.x -mBubStillRadius*sinTheta;
float mBubStillAY = mBubStillCenter.y +mBubStillRadius*cosTheta;
//B点动坐标计算
float mBubMoveBX = mBubMoveableCenter.x -mBubMoveableRadius*sinTheta;
float mBubMoveBY = mBubMoveableCenter.y +mBubMoveableRadius*cosTheta;
mBezierPath.moveTo(mBubStillAX,mBubStillAY);
mBezierPath.quadTo(iAnchorX,iAnchorY,mBubMoveBX,mBubMoveBY);
3.状态判断OnTouch
在onTouchEvent()
中,对touch事件进行自己处理,即return true
进行以下几个状态切换.
- 按下, 如果 当前状态 没有消失, 则计算 当前气泡距离,和 最大允许偏移量 作比较 ,进行状态切换
- 移动 ,改变动点的位置, 计算两点距离, 改变 静止点的半径,切换状态,并重绘制,调用ondraw
- 抬起手, 根据 链接 状态, 开始进行 动画
4.绘制不同 OnDraw
根据上述不同的状态,绘制不同的view.
- 1当 当前状态 != 消失, 则, 画 移动的气泡 和文字
- 2.相连的气泡状态,绘制贝塞尔曲线
- 3.boom,爆炸状态, 爆炸效果动画
最终效果
实现源码
/**
* 仿QQ拖拽气泡
* Created by chenchangjun on 17/5/25.
*
*/
public class DragBuubbleView extends View {
public Context mContext;
/**
* state 根据 ontouch事件进行判断,
*/
private final int STATE_DEFAULT = 0;// 1.气泡静止状态 --气泡小球和数字
private final int STATE_CONECTED = 1;//2.气泡相连 -- 两个相连的气泡小球(带黏连效果),和数字
private final int STATE_APART = 2;//3.气泡分离 -- 单个气泡小球的拖动
private final int STATE_DISMISS = 3;//4.气泡消失 -- 爆炸
//default values;
private float default_radius;
private int default_bubble_color;
private String default_bubble_text;
private int default_bubble_textColor;
private float default_bubble_textSize;
private float default_bubble_radius;
private float bubble_radius;//小球半径
private int bubble_color;//小球颜色
private String bubble_text;//小球显示字
private int bubble_textColor; //字体颜色
private float bubble_textSize;//字体大小
/**
* 静止气泡和动态气泡相关
*/
private float mBubStillRadius;//不动气泡的半径
private float mBubMoveableRadius;//可动气泡的半径
private PointF mBubStillCenter;//不动气泡的圆心
private PointF mBubMoveableCenter;//可动气泡的圆心
private Paint mBubblePaint;//气泡的画笔
/**
* 贝塞尔曲线path相关
*/
private Path mBezierPath;
private Paint mTextPaint;
private Rect mTextRect;
private Paint mBurstPaint;
private Rect mBurstRect;
private int current_state = STATE_DEFAULT;//当前状态
private float bubbleDistance; //气泡间距
private float maxDistance; //气泡最大间距
private float MOVE_OFFSET = 0;//允许你的 手指触摸偏移量
private Bitmap[] burstBitmapsArray;//气泡爆炸bitmap数组
private boolean isBurstAnimStart = false; //是否执行 气泡爆炸
private int curDrawableIndex; //当前气泡爆炸图片index
//气泡爆炸的图片id数组
private int[] burstDrawablesArray = {R.drawable.burst_1, R.drawable.burst_2
, R.drawable.burst_3, R.drawable.burst_4, R.drawable.burst_5};
private void init(Context context, AttributeSet attrs) {
this.mContext = context;
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView);
bubble_radius = typedArray.getDimension(R.styleable.DragBubbleView_bubble_radius, default_bubble_radius);
bubble_color = typedArray.getColor(R.styleable.DragBubbleView_bubble_color, default_bubble_color);
bubble_textSize = typedArray.getDimension(R.styleable.DragBubbleView_bubble_textSize, default_bubble_textSize);
bubble_textColor = typedArray.getColor(R.styleable.DragBubbleView_bubble_textColor, default_bubble_textColor);
bubble_text = typedArray.getString(R.styleable.DragBubbleView_bubble_text);
typedArray.recycle();
}
mBubStillRadius = bubble_radius;
mBubMoveableRadius = mBubStillRadius;
maxDistance = 8 * bubble_radius;
MOVE_OFFSET = maxDistance / 4;
//气泡画笔
mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBubblePaint.setColor(bubble_color);
mBubblePaint.setStyle(Paint.Style.FILL);
mBezierPath = new Path();
//文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(bubble_textColor);
mTextPaint.setTextSize(bubble_textSize);
mTextRect = new Rect();
//爆炸画笔
mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBurstPaint.setFilterBitmap(true);
mBurstRect = new Rect();
burstBitmapsArray = new Bitmap[burstDrawablesArray.length];
for (int i = 0; i < burstDrawablesArray.length; i++) {
//将气泡爆炸的drawable转为bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), burstDrawablesArray[i]);
burstBitmapsArray[i] = bitmap;
}
}
private void initView(int w, int h) {
//设置两气泡圆心初始坐标
if (mBubStillCenter == null) {
mBubStillCenter = new PointF(w / 2, h / 2);
} else {
mBubStillCenter.set(w / 2, h / 2);
}
//设置动点坐标
if (mBubMoveableCenter == null) {
mBubMoveableCenter = new PointF(w / 2, h / 2);
} else {
mBubMoveableCenter.set(w / 2, h / 2);
}
current_state = STATE_DEFAULT;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//当 当前状态 != 消失, 则, 画 移动的气泡 和文字
if (current_state != STATE_DISMISS) {
canvas.drawCircle(mBubMoveableCenter.x,mBubMoveableCenter.y,
mBubMoveableRadius,mBubblePaint);
mTextPaint.getTextBounds(bubble_text, 0, bubble_text.length(), mTextRect);
canvas.drawText(bubble_text, mBubMoveableCenter.x - mTextRect.width() / 2, mBubMoveableCenter.y + mTextRect.height() / 2, mTextPaint);
}
//2.相连的气泡状态,绘制贝塞尔曲线
if (current_state == STATE_CONECTED) {
//1.静止气泡
canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y, mBubStillRadius, mBubblePaint);
//重置path状态
mBezierPath.reset();
//2.相连曲线---贝塞尔曲线
//计算控制点坐标---两个球圆心的中点.
int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);
int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);
//计算 三角函数
float cosTheta = (mBubStillCenter.x - mBubMoveableCenter.x)/bubbleDistance;
float sinTheta = (mBubStillCenter.y - mBubMoveableCenter.y)/bubbleDistance;
//画上半弧 B-->A---------------
//A点静坐标计算
float mBubStillAX = mBubStillCenter.x -mBubStillRadius*sinTheta;
float mBubStillAY = mBubStillCenter.y +mBubStillRadius*cosTheta;
//B点动坐标计算
float mBubMoveBX = mBubMoveableCenter.x -mBubMoveableRadius*sinTheta;
float mBubMoveBY = mBubMoveableCenter.y +mBubMoveableRadius*cosTheta;
mBezierPath.moveTo(mBubStillAX,mBubStillAY);
mBezierPath.quadTo(iAnchorX,iAnchorY,mBubMoveBX,mBubMoveBY);
//画下半弧 C-->D-------------
//C点动坐标计算
float mBubMoveCX = mBubMoveableCenter.x +mBubMoveableRadius*sinTheta;
float mBubMoveCY = mBubMoveableCenter.y -mBubMoveableRadius*cosTheta;
//D点静坐标计算
float mBubStillDX = mBubStillCenter.x +mBubStillRadius*sinTheta;
float mBubStillDY = mBubStillCenter.y -mBubStillRadius*cosTheta;
mBezierPath.lineTo(mBubMoveCX,mBubMoveCY);
mBezierPath.quadTo(iAnchorX,iAnchorY,mBubStillDX,mBubStillDY);
//释放资源,并画----------
mBezierPath.close();
canvas.drawPath(mBezierPath,mBubblePaint);
}
//boom,爆炸状态, 爆炸效果动画
if(isBurstAnimStart){
mBurstRect.set((int)(mBubMoveableCenter.x - mBubMoveableRadius),
(int)(mBubMoveableCenter.y - mBubMoveableRadius),
(int)(mBubMoveableCenter.x + mBubMoveableRadius),
(int)(mBubMoveableCenter.y + mBubMoveableRadius));
canvas.drawBitmap(burstBitmapsArray[curDrawableIndex],null,
mBurstRect,mBubblePaint); }
}
/**
* 重置状态
*/
public void reset(){
initView(getWidth(),getHeight());
invalidate();
}
/**
* 开始气泡 重置状态 的动画
*/
private void startBubbleRestAnim() {
ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
new PointF(mBubMoveableCenter.x,mBubMoveableCenter.y),
new PointF(mBubStillCenter.x,mBubStillCenter.y));
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator(5f));
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBubMoveableCenter = (PointF) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
current_state= STATE_DEFAULT;
}
});
anim.start();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initView(w, h);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
//按下, 如果 当前状态 没有消失, 则计算 当前气泡距离,和 最大允许偏移量 作比较 ,进行状态切换
case MotionEvent.ACTION_DOWN:
if(current_state!=STATE_DISMISS){
bubbleDistance=(float) Math.hypot(event.getX()-mBubStillCenter.x,event.getY()-mBubStillCenter.y);
if (bubbleDistance<bubble_radius+MOVE_OFFSET){
current_state=STATE_CONECTED;
}else {
current_state=STATE_DEFAULT;
}
}
break;
//移动 ,改变动点的位置, 计算两点距离, 改变 静止点的半径,切换状态,并重绘制,调用ondraw
case MotionEvent.ACTION_MOVE:
{
if(current_state != STATE_DEFAULT){
mBubMoveableCenter.x = event.getX();
mBubMoveableCenter.y = event.getY();
bubbleDistance = (float) Math.hypot(event.getX() - mBubStillCenter.x,
event.getY() - mBubStillCenter.y);
if(current_state == STATE_CONECTED){
// 减去MOVE_OFFSET是为了让不动气泡半径到一个较小值时就直接消失
// 或者说是进入分离状态
if(bubbleDistance < maxDistance - MOVE_OFFSET){
mBubStillRadius = bubble_radius - bubbleDistance / 8;
}else{
current_state = STATE_APART;
}
}
invalidate();
}
}
break;
//抬起手, 根据 链接 状态, 开始进行 动画
case MotionEvent.ACTION_UP:
{
if(current_state == STATE_CONECTED){
startBubbleRestAnim();
}else if(current_state == STATE_APART){
if(bubbleDistance < 2 * bubble_radius){
startBubbleRestAnim();
}else{
startBubbleBurstAnim();
}
}
}
break;
}
return true;
}
/**
* 气泡消失动画
*/
private void startBubbleBurstAnim() {
//气泡改为消失状态
current_state = STATE_DISMISS;
isBurstAnimStart = true;
//做一个int型属性动画,从0~mBurstDrawablesArray.length结束
ValueAnimator anim = ValueAnimator.ofInt(0, burstDrawablesArray.length);
anim.setInterpolator(new LinearInterpolator());
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//设置当前绘制的爆炸图片index
curDrawableIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//修改动画执行标志
isBurstAnimStart = false;
}
});
anim.start();
}
public DragBuubbleView(Context context) {
super(context);
init(context, null);
}
public DragBuubbleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public DragBuubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DragBuubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
}