1.概述
贝塞尔曲线的基本应用,仿QQ消息拖拽
2.效果实现
2.1 效果分析:
上一张图片看下:

Aug-03-2020 11-09-01.gif
上面这个效果就比较简单了,先分析一下实现方式。我手指在任何一个位置触摸拖动都会是如上图的这个样式,这个实现的起来就相对简单许多了:
2.1.1: 手指按下拖动的时候有一个拖拽的圆这个圆的半径是不会变化的但是位置会随着手指移动;
2.1.2: 手指按下拖动的时候有一个固定的圆这个圆的是会变化的但是位置不会变化,圆的半径取决于两个圆的距离,两个圆的距离越大圆半径越小,距离越小圆半径越大;
2.1.2: 两个圆之间有一个不规则的东西将两个圆连在一起感觉像粘液一样,这就是大家所说的贝塞尔效果。
2.2 效果实现
2.2.1: 监听触摸绘制两个圆
我们先挑简单的写,首先监听手指触摸不断的绘制两个圆(固定圆和拖拽圆)
/**
* Author: 信仰年轻
* Date: 2020-08-03 10:02
* Email: hydznsqk@163.com
* Des: 仿qq消息拖拽 - 贝塞尔曲线
* 思路:
* 1.先把拖动圆的半径初始化出来,固定圆可以额外给出来最大半径和最小半径,拖拽圆的半径因为是会变化的,需要动态计算
* 2.固定不动的圆会随着拖动的圆的距离增大而变小,直到中间的线拉断然后不可见
* 3.手指down的时候,先获取到手指的坐标,然后初始化固定圆和拖拽圆的 PointF
* 4.手指MOVE的时候,更新拖拽圆的PointF的坐标
* 5.然后画出来两个圆,一个固定不动的圆,一个可以拖动的圆,
* 6.因为固定圆会变小,固定不动的圆的半径需要动态计算,可以让 固定圆最大半径 - 固定圆和拖拽圆的距离 / 14 得到固定圆的半径
* 7.如果该固定圆的半径 < 最初给定的最小半径值 就不画了(异味着消失)
* 8.然后就是画贝塞尔曲线,需要求出来两条线和两个圆的相交点的坐标,然后控制点取两个圆连线的中心点
* 9.如何求这4个坐标就用到了三角函数(初中水平的数学)
*/
public class MessageBubbleView extends View {
//拖动圆的半径
private int mDragRadius = 10;
//固定圆的半径,最大半径,最小半径
private int mFixedRadiusMax = 7;
private int mFixedRadiusMin = 3;
private int mFixedRadius;
private PointF mFixedPoint;
private PointF mDragPoint;
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);
mDragRadius = dp2sp(mDragRadius);
mFixedRadiusMax = dp2sp(mFixedRadiusMax);
mFixedRadiusMin = dp2sp(mFixedRadiusMin);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float downX = event.getX();
float downY = event.getY();
initPointF(downX, downY);
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
float moveY = event.getY();
updateDragPointF(moveX, moveY);
break;
case MotionEvent.ACTION_UP:
break;
}
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
if (mFixedPoint == null || mDragPoint == null) {
return;
}
//画拖拽圆
canvas.drawCircle(mDragPoint.x, mDragPoint.y, mDragRadius, mPaint);
Path path = initBezier();
if (path != null) {
//画固定圆
canvas.drawCircle(mFixedPoint.x, mFixedPoint.y, mFixedRadius, mPaint);
//画贝塞尔曲线
canvas.drawPath(path, mPaint);
}
}
private Path initBezier() {
// 如果 固定圆半径 < 设定的固定圆半径最小值,就不画了
double distance = getDistance(mFixedPoint, mDragPoint);
//固定圆半径 = 固定圆的最大半径 - 距离 / 14
mFixedRadius = (int) (mFixedRadiusMax - distance / 14);
if (mFixedRadius < mFixedRadiusMin) {
return null;
}
//初始化贝塞尔的path ,贝塞尔曲线需要一个控制点,两个圆心的中心点
Path bezierPath = new Path();
// 求角 a
// 求斜率
float dy = (mDragPoint.y - mFixedPoint.y);
float dx = (mDragPoint.x - mFixedPoint.x);
float tanA = dy / dx;
// 求角a的角度的度数
double arcTanA = Math.atan(tanA);
// p0
float p0x = (float) (mFixedPoint.x + mFixedRadius * Math.sin(arcTanA));
float p0y = (float) (mFixedPoint.y - mFixedRadius * Math.cos(arcTanA));
// p1
float p1x = (float) (mDragPoint.x + mDragRadius * Math.sin(arcTanA));
float p1y = (float) (mDragPoint.y - mDragRadius * Math.cos(arcTanA));
// p2
float p2x = (float) (mDragPoint.x - mDragRadius * Math.sin(arcTanA));
float p2y = (float) (mDragPoint.y + mDragRadius * Math.cos(arcTanA));
// p3
float p3x = (float) (mFixedPoint.x - mFixedRadius * Math.sin(arcTanA));
float p3y = (float) (mFixedPoint.y + mFixedRadius * Math.cos(arcTanA));
// 拼装 贝塞尔的曲线路径
bezierPath.moveTo(p0x, p0y); // 移动
// 两个点
PointF controlPoint = getControlPoint();
// 画了第一条 第一个点(控制点,两个圆心的中心点),终点
bezierPath.quadTo(controlPoint.x, controlPoint.y, p1x, p1y);
// 画第二条
bezierPath.lineTo(p2x, p2y); // 链接到
bezierPath.quadTo(controlPoint.x, controlPoint.y, p3x, p3y);
bezierPath.close();
return bezierPath;
}
/**
* 获取两个点的中间距离,用勾股定理可以算出来
*
* @param fixedPoint
* @param dragPoint
* @return
*/
private double getDistance(PointF fixedPoint, PointF dragPoint) {
// (固定圆x - 拖拽圆x) * (固定圆x - 拖拽圆x) + (固定圆y - 拖拽圆y) * (固定圆y - 拖拽圆y) 开根号
double sqrt = Math.sqrt((fixedPoint.x - dragPoint.x) * (fixedPoint.x - dragPoint.x) + (fixedPoint.y - dragPoint.y) * (fixedPoint.y - dragPoint.y));
return sqrt;
}
private void initPointF(float downX, float downY) {
mFixedPoint = new PointF(downX, downY);
mDragPoint = new PointF(downX, downY);
}
private void updateDragPointF(float moveX, float moveY) {
mDragPoint.x = moveX;
mDragPoint.y = moveY;
}
public PointF getControlPoint() {
return new PointF((mDragPoint.x + mFixedPoint.x) / 2, (mDragPoint.y + mFixedPoint.y) / 2);
}
private int dp2sp(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
}
2.2.2: 绘制贝塞尔曲线
贝塞尔曲线绘制起来有点小麻烦,我没记错的话是初中的数学知识,如果你不是特别了解贝塞尔曲线和三角函数可以百度一下,这里给两个链接 贝塞尔曲线 和 三角函数 文章中我就不做过多的解释,下面讲一下求解思路:

贝塞尔曲线.jpg
看上面这张图画得不咋地但纯手工,蓝色部分和黑色部分是已知,黄色部分是辅助线是可以利用三角公式求出来的,红色部分是未知。我们只要求得角 a,有了角 a 我们就能求 x 和 y 这样我们就知道了 p0 的位置,依葫芦画瓢求能求得 p0,p1,p2,p3的值,有了四个点有了控制点自然就能画贝塞尔曲线了。
/**
* 初始化Bezier曲线
* @return
*/
private Path initBezier() {
// 如果 固定圆半径 < 设定的固定圆半径最小值,就不画了
double distance = getDistance(mFixedPoint, mDragPoint);
//固定圆半径 = 固定圆的最大半径 - 距离 / 14
mFixedRadius = (int) (mFixedRadiusMax - distance / 14);
if (mFixedRadius < mFixedRadiusMin) {
return null;
}
//初始化贝塞尔的path ,贝塞尔曲线需要一个控制点,两个圆心的中心点
Path bezierPath = new Path();
// 求角 a
// 求斜率
float dy = (mDragPoint.y - mFixedPoint.y);
float dx = (mDragPoint.x - mFixedPoint.x);
float tanA = dy / dx;
// 求角a的角度的度数
double arcTanA = Math.atan(tanA);
// p0
float p0x = (float) (mFixedPoint.x + mFixedRadius * Math.sin(arcTanA));
float p0y = (float) (mFixedPoint.y - mFixedRadius * Math.cos(arcTanA));
// p1
float p1x = (float) (mDragPoint.x + mDragRadius * Math.sin(arcTanA));
float p1y = (float) (mDragPoint.y - mDragRadius * Math.cos(arcTanA));
// p2
float p2x = (float) (mDragPoint.x - mDragRadius * Math.sin(arcTanA));
float p2y = (float) (mDragPoint.y + mDragRadius * Math.cos(arcTanA));
// p3
float p3x = (float) (mFixedPoint.x - mFixedRadius * Math.sin(arcTanA));
float p3y = (float) (mFixedPoint.y + mFixedRadius * Math.cos(arcTanA));
// 拼装 贝塞尔的曲线路径
bezierPath.moveTo(p0x, p0y); // 移动
// 两个点
PointF controlPoint = getControlPoint();
// 画了第一条 第一个点(控制点,两个圆心的中心点),终点
bezierPath.quadTo(controlPoint.x, controlPoint.y, p1x, p1y);
// 画第二条
bezierPath.lineTo(p2x, p2y); // 链接到
bezierPath.quadTo(controlPoint.x, controlPoint.y, p3x, p3y);
bezierPath.close();
return bezierPath;
}