效果图
要实现这样的效果 我们要先知道那段弧线是如何绘制的,实际上那段弧线就是贝塞尔曲线
一. 什么是贝塞尔曲线
https://www.jianshu.com/p/8f82db9556d2
上面有个链接 里面的动图比较形象
二阶Bezier曲线公式:
P0A/P1A=P1B/P2B=AC/BC
在维持以上比例的同时,A点从P0出发到达P1点
点C所经过的路径就是Bezier曲线
其中P1叫做控制点,控制点不同,绘制的路径也会大有不同
当控制点有0个时,点C的路径叫1阶Bezier曲线
当控制点有1个时,点C的路径叫2阶Bezier曲线
当控制点有2个时,点C的路径叫3阶Bezier曲线
以此类推
这里我们只需要了解2阶Bezier曲线即可
最后绘制路径大致如下
图上t表示比值 说了一堆东西 其实要用的东西很简单
二.分析实现思路
2.1 有两个圆,一个是固定圆,位置固定不动但是半径会变化(两个圆之间的距离越远半径就越小)
还有一个是拖拽圆,半径是不变的,位置是跟随手指移动
遇到问题:getRawY的坐标似乎不对 似乎差一个状态栏的高度
解决:使用getY代替 getY是相对于当前的View的Y getRawY是相对于android.id.content的Y,注意不包含状态栏和titileBar
2.2 在两个圆之间中间会有一个粘性的不规则图像(贝塞尔曲线)
求法如下,需要用到简单的三角函数
已知r1,r2的坐标,大圆与小圆的半径,A1B1和A2B2分别垂直于r1r2,求A1A2B1B2四点坐标
乍一看好像没什么头绪,让我们画几个辅助线
如图 ∠C1和∠C2都是直角
那么A1的坐标可以表示为 (r1.x+r1C1,r1.y+r1y+A1C1)
B1B2A2三点的坐标类似
且三角形r1C1A1相似于三角形r1C2r2
那么∠r1A1C1=∠r1r2C2=∠a
我们可以求得∠r1r2C2 即∠a
(r1.y-r2.y)/(r1.x-r2.x)=tan∠r1r2C2
那么∠a = arctan((r1.y-r2.y)/(r1.x-r2.x))
r1C1/r1A1=sin∠a
为了简便 我们记r1C1=x A1C1=y
x=sin∠a小圆半径
y=cos∠a小圆半径
那么x=sin(arctan((r1.y-r2.y)/(r1.x-r2.x)))r1A1
y=sin(arctan((r1.y-r2.y)/(r1.x-r2.x)))r1A1
A1B1的坐标分别表示为
r1x+x,r1y+y
r1x-x,r1y-y
计算A2B2的算法类似 只不过此时应该使用r2的半径
x’=sin∠a大圆半径
y’=cos∠a大圆半径
接下来计算控制点的位置 我们取A1A2连线的中心点以及B1B2的中心点
其实最复杂的是Bezier曲线的绘制,但是Android已经给我们写好了方法 我们直接调用即可,即Path的quadTo方法
坑:这里面涉及很多加加减减,一不小心写错了,效果不对的时候排查很难,所以写的时候就得小心。这次我就栽在这里,写错了两个±号,又找了40分钟。。。
最终实现效果大致这样
三.部分代码
自定view:
class BezierView extends View {
private PointF mFixPoint;
private PointF mFingerPoint;
private Paint mPaint;
private float mFixPointInitRadius = 10;//固定圆初始半径
private float mFixPointChangedRadius;//挪动手指后的固定圆半径
private float mFingerPointRadius = 10;
public BezierView(Context context) {
this(context, null);
}
public BezierView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BezierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
mPaint = new Paint();
mPaint.setDither(true);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
if (mFingerPoint == null || mFixPoint == null) {
return;
}
//绘制固定圆
if (mFixPointChangedRadius > 5) {//当手指离固定圆太远 不绘制固定圆
canvas.drawCircle(mFixPoint.x, mFixPoint.y, mFixPointChangedRadius, mPaint);
canvas.drawPath(getBezierPath(), mPaint);
}
//绘制跟随手指的圆
canvas.drawCircle(mFingerPoint.x, mFingerPoint.y, mFingerPointRadius, mPaint);
}
//计算两点之间的距离
double distanceOfPoints(PointF point1, PointF point2) {
//勾股定理
return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mFixPoint = new PointF();
mFingerPoint = new PointF();
mFixPoint.x = event.getX();
mFixPoint.y = event.getY();
mFingerPoint.x = event.getX();
mFingerPoint.y = event.getY();
case MotionEvent.ACTION_MOVE:
mFingerPoint.x = event.getX();
mFingerPoint.y = event.getY();
break;
case MotionEvent.ACTION_UP:
break;
}
//根据手指的移动 计算两点之间的距离 根据距离改变mFixPoint的半径
mFixPointChangedRadius = calculateFixPointRadius(distanceOfPoints(mFingerPoint, mFixPoint));
invalidate();
return true;
}
private float calculateFixPointRadius(double distanceOfPoints) {
return mFixPointInitRadius - (float) distanceOfPoints / 25f;//除数(20f)越大,代表半径随距离变化的程度越小
}
private Path getBezierPath() {
Path bezierPath = new Path();
//计算四点坐标
//先计算∠a ∠a = arctan((r1.y-r2.y)/(r1.x-r2.x))
double angleA = Math.atan((mFixPoint.y - mFingerPoint.y) / (mFixPoint.x - mFingerPoint.x));
//计算x y以及 x' y'
float x = (float) (Math.sin(angleA) * mFixPointChangedRadius);
float y = (float) (Math.cos(angleA) * mFixPointChangedRadius);
float x2 = (float) (Math.sin(angleA) * mFingerPointRadius);
float y2 = (float) (Math.cos(angleA) * mFingerPointRadius);
//四个点坐标分别为 A1 B1 A2 B2 他们坐标表示为
float A1x = mFixPoint.x + x;
float A1y = mFixPoint.y - y;
float B1x = mFixPoint.x - x;
float B1y = mFixPoint.y + y;
float A2x = mFingerPoint.x + x2;
float A2y = mFingerPoint.y - y2;
float B2x = mFingerPoint.x - x2;
float B2y = mFingerPoint.y + y2;
float controlPint1x = (mFixPoint.x + mFingerPoint.x) * 0.5f;
float controlPint1y = (mFixPoint.y + mFingerPoint.y) * 0.5f;
//绘制路径为A1 A2 B2 B1
bezierPath.moveTo(A1x, A1y);
bezierPath.quadTo(controlPint1x, controlPint1y, A2x, A2y);
bezierPath.lineTo(B2x, B2y);
bezierPath.quadTo(controlPint1x, controlPint1y, B1x, B1y);
bezierPath.close();
return bezierPath;
}
}
后记
其实我觉得贝塞尔曲线的绘制四个点有一点问题,应该如下图所示:
这样计算应该更正确,不过这样计算更复杂(主要是不会算),而且效果应该差不了多少,想想还是放弃了,就直接用的上面的计算方式计算贝塞尔曲线的四个点