1. 说明
贝塞尔曲线效果,大家应该不陌生,而且大家肯定都见过,就比如我们的QQ消息拖拽,它就是我们的贝塞尔曲线实现的,其实还有一些效果,比如一些轮播图的指示器、下拉刷新的一些控件、花束直播点赞效果等等的一些效果。而实现这些效果其实都是使用的是贝塞尔曲线的,而这个也涉及到我们的数学知识。还有我们的三角函数。那么我们今天先来实现一个简单的效果,在任何地方按下,都会有两个圆,一个大、一个小,然后让手指在屏幕上边拖动,并且在两个圆中间带有粘性的效果,并且可以任意拖动,效果图如下:
图片.png
2. 贝塞尔曲线
我们先来看下贝塞尔曲线是什么样的,并且它有什么样的特点:
它的特点就是:
在第一条线段 P0P1 上边任取一个点D,然后在 P1P2 上边取一点F,让P1P0/DP0 = DF/DE = P1P2/P1F,而这三者的比值是相等的,就可以,如图所示:
二阶贝塞尔曲线认识.png
2. 思路分析
2.1 有2个圆,一个圆固定不动但是半径会变化,两个圆之间的距离越远,该固定圆半径就越小;还有一个可拖拽圆,半径是不变的,位置是跟随手指移动;
两点之间距离的计算方式如下图所示:
两点之间的距离算法.png
2.2 并且在两个圆中间带有粘性的不规则图像,这个不规则图像就叫做贝塞尔曲线;
获取贝塞尔曲线路径计算方式如下图所示:
获取贝塞尔路径.png
3. 代码如下
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/3/10 9:56
* Version 1.0
* Params:
* Description: 两个圆及贝塞尔曲线的画法
*/
public class MessageBubbleView extends View {
// 2个圆的圆心
private PointF mFixationPoint , mDragPoint ;
// 画笔
private Paint mPaint ;
// 拖拽圆的半径
private int mDragRadius = 10 ;
// 固定圆最大半径,即就是初始半径
private int mFixationRadiusMax = 7 ;
// 固定圆最小半径
private int mFixationRadiusMin = 3 ;
private int mFixationRadius ;
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 = dip2px(mDragRadius) ;
// 固定圆最大半径
mFixationRadiusMax = dip2px(mFixationRadiusMax) ;
// 固定圆最小半径
mFixationRadiusMin = dip2px(mFixationRadiusMin) ;
mPaint = new Paint() ;
mPaint.setColor(Color.RED);
// 设置抗锯齿
mPaint.setAntiAlias(true);
// 设置仿抖动
mPaint.setDither(true);
}
/**
* 手指触摸屏幕,会触发onTouchEvent方法
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
// 按下的时候指定当前的位置,即就是圆心位置
float downX = event.getX() ;
float downY = event.getY() ;
// 初始化圆心位置
initPoint(downX , downY);
break;
// 根据手指移动,不断的更新当前位置
case MotionEvent.ACTION_MOVE:
float moveX = event.getX() ;
float moveY = event.getY() ;
// 更新当前拖拽点的位置
updateDragPoint(moveX , moveY) ;
break;
case MotionEvent.ACTION_UP:
break;
}
// 只要调用invalidate(),就会调用onDraw()方法,会去画两个圆
invalidate();
return true;
}
/**
* 更新当前拖拽点的位置
* @param moveX
* @param moveY
*/
private void updateDragPoint(float moveX, float moveY) {
mDragPoint.x = moveX ;
mDragPoint.y = moveY ;
}
/**
* 初始化圆心位置
* @param downX
* @param downY
*/
private void initPoint(float downX, float downY) {
// 可拖拽圆的圆心位置
mDragPoint = new PointF(downX , downY) ;
// 固定圆的圆心位置
mFixationPoint = new PointF(downX , downY) ;
}
@Override
protected void onDraw(Canvas canvas) {
if (mDragPoint == null || mFixationPoint == null){
return;
}
// 画2个圆
// 画拖拽圆
canvas.drawCircle(mDragPoint.x , mDragPoint.y , mDragRadius , mPaint);
// 画固定圆
// 有一个初始化的大小 而且半径是随着距离的增大而减小 并且小到一定程度就不见了,意思就是不画了
// 计算小圆半径,就是计算两个点的距离
double distance = getDistance(mDragPoint , mFixationPoint) ;
Path bezeierPath = getBezeierPath() ;
if (bezeierPath != null){
// 画固定圆
canvas.drawCircle(mFixationPoint.x , mFixationPoint.y , mFixationRadius , mPaint);
// 画贝塞尔曲线
canvas.drawPath(bezeierPath , mPaint);
}
}
/**
* 获取两个圆之间的距离
* @param point1
* @param point2
* @return
*/
private double getDistance(PointF point1 , PointF point2){
return Math.sqrt((point1.x- point2.x) * (point1.x- point2.x) + (point1.y- point2.y) * (point1.y- point2.y)) ;
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP , dip , getResources().getDisplayMetrics());
}
/**
* 获取贝塞尔的路径
* @return
*/
public Path getBezeierPath() {
double distance = getDistance(mDragPoint, mFixationPoint);
// 固定圆的半径
// 表示 distance越大 distance/14就越大 , mFixationRadiusMax - distance/14这个差值就越小
mFixationRadius = (int) (mFixationRadiusMax - distance / 14);
if (mFixationRadius < mFixationRadiusMin) {
// 超过一定距离 贝塞尔和固定圆都不要画了
return null;
}
Path bezeierPath = new Path();
// 求角 a
// 求斜率
float dy = (mDragPoint.y-mFixationPoint.y);
float dx = (mDragPoint.x-mFixationPoint.x);
float tanA = dy/dx;
// 求角a
double arcTanA = Math.atan(tanA);
// p0
float p0x = (float) (mFixationPoint.x + mFixationRadius*Math.sin(arcTanA));
float p0y = (float) (mFixationPoint.y - mFixationRadius*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) (mFixationPoint.x - mFixationRadius*Math.sin(arcTanA));
float p3y = (float) (mFixationPoint.y + mFixationRadius*Math.cos(arcTanA));
// 拼装 贝塞尔的曲线路径
bezeierPath.moveTo(p0x,p0y); // 移动
// 两个点
PointF controlPoint = getControlPoint();
// 画了第一条 第一个点(控制点,两个圆心的中心点),终点
bezeierPath.quadTo(controlPoint.x,controlPoint.y,p1x,p1y);
// 画第二条
bezeierPath.lineTo(p2x,p2y); // 链接到
bezeierPath.quadTo(controlPoint.x,controlPoint.y,p3x,p3y);
bezeierPath.close();
return bezeierPath;
}
/**
* 获取控制点
* @return
*/
public PointF getControlPoint() {
return new PointF((mDragPoint.x+mFixationPoint.x)/2,(mDragPoint.y+mFixationPoint.y)/2);
}
}
4. 注意
其中需要注意的地方:
4.1 画2个圆的方法:
// 画拖拽圆
canvas.drawCircle(mDragPoint.x , mDragPoint.y , mDragRadius , mPaint);
// 画固定圆
canvas.drawCircle(mFixationPoint.x , mFixationPoint.y , mFixationRadius , mPaint);
4.2 获取两个圆之间的距离:
/**
* 获取两个圆之间的距离
* @param point1
* @param point2
* @return
*/
private double getDistance(PointF point1 , PointF point2){
return Math.sqrt((point1.x- point2.x) * (point1.x- point2.x) + (point1.y- point2.y) * (point1.y- point2.y)) ;
}
4.3 获取贝塞尔曲线的方法:
/**
* 获取贝塞尔的路径
* @return
*/
public Path getBezeierPath() {
double distance = getDistance(mDragPoint, mFixationPoint);
// 固定圆的半径
// 表示 distance越大 distance/14就越大 , mFixationRadiusMax - distance/14这个差值就越小
mFixationRadius = (int) (mFixationRadiusMax - distance / 14);
if (mFixationRadius < mFixationRadiusMin) {
// 超过一定距离 贝塞尔和固定圆都不要画了
return null;
}
Path bezeierPath = new Path();
// 求角 a
// 求斜率
float dy = (mDragPoint.y-mFixationPoint.y);
float dx = (mDragPoint.x-mFixationPoint.x);
float tanA = dy/dx;
// 求角a
double arcTanA = Math.atan(tanA);
// p0
float p0x = (float) (mFixationPoint.x + mFixationRadius*Math.sin(arcTanA));
float p0y = (float) (mFixationPoint.y - mFixationRadius*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) (mFixationPoint.x - mFixationRadius*Math.sin(arcTanA));
float p3y = (float) (mFixationPoint.y + mFixationRadius*Math.cos(arcTanA));
// 拼装 贝塞尔的曲线路径
bezeierPath.moveTo(p0x,p0y); // 移动
// 两个点
PointF controlPoint = getControlPoint();
// 画了第一条 第一个点(控制点,两个圆心的中心点),终点
bezeierPath.quadTo(controlPoint.x,controlPoint.y,p1x,p1y);
// 画第二条
bezeierPath.lineTo(p2x,p2y); // 链接到
bezeierPath.quadTo(controlPoint.x,controlPoint.y,p3x,p3y);
bezeierPath.close();
return bezeierPath;
}
4.4 获取控制点的方法:
/**
* 获取控制点
* @return
*/
public PointF getControlPoint() {
return new PointF((mDragPoint.x+mFixationPoint.x)/2,(mDragPoint.y+mFixationPoint.y)/2);
}
代码已上传至github:
https://github.com/shuai999/View_day24.git