贝塞尔曲线 - QQ消息汽包拖拽
1.概述
消息气泡拖拽资料有很多,网上也有开源代码,下载下来就可以用。为什么还要折腾呢?我想证明一下数学已经初中毕业,其次像贝塞尔这种效果还是很常见的,虽然目前我只有一个 APP 用了这个效果。我想一行代码让所有的控件都可以拖动爆炸,不是为了重复造轮子而是为了装B。
QQ消息汽包拖拽
2.效果实现
** 2.1 效果分析 **
看上面的效果感觉有点麻烦,怎么做到任何控件都可以拖动爆炸,我想说网上应该仅此一家。首先可以不要搞得这么麻烦,比如我再上一张图片看下:
简单版.gif
上面这个效果就比较简单了,先分析一下实现方式。我手指在任何一个位置触摸拖动都会是如上图的这个样式,这个实现的起来就相对简单许多了:
2.1.1: 手指按下拖动的时候有一个拖拽的圆这个圆的半径是不会变化的但是位置会随着手指移动;
2.1.2: 手指按下拖动的时候有一个固定的圆这个圆的是会变化的但是位置不会变化,圆的半径取决于两个圆的距离,两个圆的距离越大圆半径越小,距离越小圆半径越大;
2.1.2: 两个圆之间有一个不规则的东西将两个圆连在一起感觉像粘液一样,这就是大家所说的贝塞尔效果。
2.2 效果实现
2.2.1: 监听触摸绘制两个圆
我们先挑简单的写,首先监听手指触摸不断的绘制两个圆(固定圆和拖拽圆),如果对触摸监听事件以及画笔使用不是特别熟悉,请留意看看我之前的一些自定义 View 的文章。Android进阶之旅 - 自定义View篇
/**
* description: 消息气泡拖拽的 View
* author: Darren on 2017/7/21 10:40
* email: 240336124@qq.com
* version: 1.0
*/
public class MessageBubbleView extends View {
// 拖拽圆的圆心点
private PointF mDragPoint;
// 固定圆的圆心点
private PointF mFixationPoint;
// 拖拽圆的半径
private int mDragRadius = 10;
// 固定圆的半径
private int mFixationRadius = 7;
// 固定圆的最小半径
private int FIXATION_RADIUS_MIN = 3;
// 固定圆的最大半径
private int FIXATION_RADIUS_MAX = 7;
// 用来绘制的画笔
private Paint mPaint;
public MessageBubbleView(Context context) {
this(context, null);
}
public MessageBubbleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MessageBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化画笔
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setDither(true);
mPaint.setAntiAlias(true);
// 初始化一些距离
mDragRadius = dip2px(mDragRadius);
mFixationRadius = dip2px(mFixationRadius);
FIXATION_RADIUS_MIN = dip2px(FIXATION_RADIUS_MIN);
FIXATION_RADIUS_MAX = dip2px(FIXATION_RADIUS_MAX);
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dip,getResources().getDisplayMetrics());
}
@Override
protected void onDraw(Canvas canvas) {
if (mDragPoint == null && mFixationPoint == null) {
return;
}
// 1.绘制拖拽圆
canvas.drawCircle(mDragPoint.x, mDragPoint.y, mDragRadius, mPaint);
// 计算两个圆之间的距离
int distance = BubbleUtils.getDistance(mDragPoint, mFixationPoint);
// 计算固定圆的半径,距离越大圆半径越小
mFixationRadius = FIXATION_RADIUS_MAX - distance / 14;
if (mFixationRadius > FIXATION_RADIUS_MIN) {
// 2.绘制固定圆
canvas.drawCircle(mFixationPoint.x, mFixationPoint.y, mFixationRadius, mPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initPoint(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
updateDragPoint(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
break;
}
invalidate();
return true;
}
/**
* 更新拖拽圆的位置
*
* @param x
* @param y
*/
private void updateDragPoint(float x, float y) {
mDragPoint.x = x;
mDragPoint.y = y;
}
/**
* 初始化圆的位置
*
* @param x
* @param y
*/
private void initPoint(float x, float y) {
mDragPoint = new PointF(x, y);
mFixationPoint = new PointF(x, y);
}
}
2.2.2: 绘制贝塞尔曲线
贝塞尔曲线绘制起来有点小麻烦,我没记错的话是初中的数学知识,如果你不是特别了解贝塞尔曲线和三角函数可以百度一下,这里给两个链接 贝塞尔曲线 和 三角函数 文章中我就不做过多的解释,下面讲一下求解思路:
看上面这张图画得不咋地但纯手工,蓝色部分和黑色部分是已知,黄色部分是辅助线是可以利用三角公式求出来的,红色部分是未知。我们只要求得角 a,有了角 a 我们就能求 x 和 y 这样我们就知道了 p0 的位置,依葫芦画瓢求能求得 p0,p1,p2,p3的值,有了四个点有了控制点自然就能画贝塞尔曲线了。
/**
* 获取 Bezier 曲线
*
* @return
*/
public Path getBezierPath() {
if (mFixationRadius < FIXATION_RADIUS_MIN) {
return null;
}
Path bezierPath = new Path();
// 贝塞尔曲线怎么求?
// 计算斜率
float dx = mFixationPoint.x - mDragPoint.x;
float dy = mFixationPoint.y - mDragPoint.y;
if (dx == 0) {
dx = 0.001f;
}
float tan = dy / dx;
// 获取角a度值
float arcTanA = (float) Math.atan(tan);
// 依次计算 p0 , p1 , p2 , p3 点的位置
float P0X = (float) (mFixationPoint.x + mFixationRadius * Math.sin(arcTanA));
float P0Y = (float) (mFixationPoint.y - mFixationRadius * Math.cos(arcTanA));
float P1X = (float) (mDragPoint.x + mDragRadius * Math.sin(arcTanA));
float P1Y = (float) (mDragPoint.y - mDragRadius * Math.cos(arcTanA));
float P2X = (float) (mDragPoint.x - mDragRadius * Math.sin(arcTanA));
float P2Y = (float) (mDragPoint.y + mDragRadius * Math.cos(arcTanA));
float P3X = (float) (mFixationPoint.x - mFixationRadius * Math.sin(arcTanA));
float P3Y = (float) (mFixationPoint.y + mFixationRadius * Math.cos(arcTanA));
// 求控制点 两个点的中心位置作为控制点
PointF controlPoint = BubbleUtils.getPointByPercent(mDragPoint, mFixationPoint, 0.5f);
// 整合贝塞尔曲线路径
bezierPath.moveTo(P0X, P0Y);
bezierPath.quadTo(controlPoint.x, controlPoint.y, P1X, P1Y);
bezierPath.lineTo(P2X, P2Y);
bezierPath.quadTo(controlPoint.x, controlPoint.y, P3X, P3Y);
bezierPath.close();
return bezierPath;
}
下一节我们来实现一下如何能够让任何一个 View 都能拖动消失,就像 QQ 的消息气泡一样,当然到时可能又免不了源码分析。
所有分享大纲:Android进阶之旅 - 自定义View篇