Android 自定义实现类似QQ消息贝塞尔拖拽效果BezierView
有图有真相:
思路梳理:
- 首先BezierView继承View,文中包含了7个主要的坐标点;
- 其中target点是初始化时绘制圆的圆心坐标点;touch点记录用户滑动的坐标点;
- 当触发可拖拽状态时,已target点和touch点分别画圆,半径mR在onSizeChanged计算出来。当2圆无交点时, 则需要实时计算2圆中间填充部分;
- control点是target点和touch点连线的中间坐标点;也就是2圆中间绘制曲线的控制点;
- 4个交点,以control点为中心,对target圆 和touch圆做切线实时计算出来的4个交点,对于贝塞尔公式来说他们都是数据点;
- 4个交点通过path采用二次贝塞尔曲线相连并且闭合则达到的样式就是拖拽但未脱离的图形拉伸效果;
- 4个交点的计算要使用到圆方程,已control点为中心交于target和touch圆肯定有4个交点,大概是2圆方程求其二元一次方程,二元一次方程与其中一个圆联立求交点;
- 当手指释放时,需要判断当前是否处于完全拽开状态,当未处于完全拽开状态则以target与touch点连线方向,反复直线运动几次达到弹动效果;
- 最终的效果图如gif;
代码如下:
- BezierView :
public class BezierView extends View implements Animation.AnimationListener {
private static final String TAG = "BezierView";
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* target坐标
*/
private PointF targetPoint = new PointF();
/**
* touch点坐标
*/
private PointF touchPoint = new PointF();
/**
* target坐标 与 touch点坐标 连线的中间点坐标 也就是control点
*/
private PointF controlPoint = new PointF();
/**
* Control点与2圆的4个交点
*/
private Point focusPointA = new Point();
private Point focusPointB = new Point();
private Point focusPointC = new Point();
private Point focusPointD = new Point();
/**
* 目标圆半径,也是touch圆半径
*/
private int mR;
/**
* 是否处于被拖拽状态
*/
private boolean isDrag = false;
/**
* 是否分割目标和touch圆
*/
private boolean isSplit = false;
/**
* 状态:是否清除内容
*/
private boolean isDiscard = false;
/**
* 是否处于直线动画执行状态
*/
private boolean isAnimating = false;
/**
* 可触发Drag的touch区域
*/
private RectF mDragRange;
/**
* 作为4个交点 及control点 的闭合path
*/
Path mPath = new Path();
/**
* control点 和目标中心点 距离;也是是否需要DrawPath的边界值
* 当该距离大于mR 意味着目标圆 和touch的圆无交点需要绘制中间区域
*/
private double needDrawClosePathDistance;
/**
* 可绘制的文本
*/
private String text = "99+";
TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
/**
* 文本偏移量 (使得文本居中时需要)
*/
private int mTextOffset;
/**
* 分离指数,这里以mR的为基础 ,needDrawClosePathDistance 大于 mR* mSplitRate 这2圆分离,也就是被拽开的状态
*/
private static final float mSplitRate = 2;
/**
* 最大完全拽开距离
* mSplitDistance = mSplitRate * mR;
*/
private double mSplitDistance;
public BezierView(Context context) {
this(context, null);
}
public BezierView(Context context, AttributeSet attrs) {
super(context, attrs);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
float dimension = context.getResources().getDimension(R.dimen.textSize);
textPaint.setTextSize(dimension);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setColor(Color.WHITE);
Paint.FontMetricsInt fontMetricsInt = textPaint.getFontMetricsInt();
mTextOffset = (Math.abs(fontMetricsInt.descent) - Math.abs(fontMetricsInt.ascent)) / 2;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
targetPoint.x = getWidth() / 2;
targetPoint.y = getHeight() / 2;
mR = Math.min(getWidth(), getHeight()) / 10;
mDragRange = new RectF(targetPoint.x - mR, targetPoint.y - mR, targetPoint.x + mR, targetPoint.y + mR);
mSplitDistance = mSplitRate * mR;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isDiscard) {
if (!isSplit) {
canvas.drawCircle(targetPoint.x, targetPoint.y, mR, paint);
}
if (isDrag) {
canvas.drawCircle(touchPoint.x, touchPoint.y, mR, paint);
if (!isSplit && needDrawClosePathDistance > mR) {
mPath.reset();
mPath.moveTo(focusPointA.x, focusPointA.y);
mPath.quadTo(controlPoint.x, controlPoint.y, focusPointC.x, focusPointC.y);
mPath.lineTo(focusPointD.x, focusPointD.y);
mPath.quadTo(controlPoint.x, controlPoint.y, focusPointB.x, focusPointB.y);
mPath.close();
canvas.drawPath(mPath, paint);
}
canvas.drawText(text, touchPoint.x, touchPoint.y - mTextOffset, textPaint);
}
if (!isSplit) {
if (!TextUtils.isEmpty(text)) {
canvas.drawText(text, targetPoint.x, targetPoint.y - mTextOffset, textPaint);
}
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
Log.i(TAG, "x=" + x + " ;y=" + y);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isAnimating) {
clearAnimation();
}
if (mDragRange.contains((int) x, (int) y)) {
isDrag = true;
touchPoint.x = (int) x;
touchPoint.y = (int) y;
computeControl();
}
break;
case MotionEvent.ACTION_MOVE:
if (isDrag) {
touchPoint.x = (int) x;
touchPoint.y = (int) y;
computeControl();
}
break;
case MotionEvent.ACTION_UP: {
isDiscard = needDrawClosePathDistance >= mSplitDistance;
isDrag = false;
isSplit = false;
if (!isDiscard) {
float[] xyArr = computeRightTriangleAB(targetPoint, touchPoint);
if (xyArr != null) {
TranslateAnimation animation = new TranslateAnimation(xyArr[0], -xyArr[0], xyArr[1], -xyArr[1]);
animation.setInterpolator(new AccelerateDecelerateInterpolator());
animation.setRepeatMode(Animation.REVERSE);
animation.setRepeatCount(4);
animation.setDuration(60);
animation.setAnimationListener(this);
BezierView.this.startAnimation(animation);
}
}
invalidate();
}
break;
}
if (isDrag) {
invalidate();
}
return true;
}
/**
* 直线动画以中心点向一个方向运动的距离
*/
private static final float TranslateAniLimited = 5;
/**
* 计算2点的斜率, 以TranslateAniLimited 为 C边计算直角山角形 A B边
*
* @param point1
* @param point2
* @return 返回的就是直角山角形AB 边
*/
private float[] computeRightTriangleAB(PointF point1, PointF point2) {
if (point1.x == point2.x && point1.y == point2.y) {
return null;
}
float[] data = new float[2];
if (point1.x == point2.x) {
data[0] = 0;
data[1] = TranslateAniLimited;
return data;
}
if (point1.y == point2.y) {
data[0] = TranslateAniLimited;
data[1] = 0;
return data;
}
float k = (point2.y - point1.y) / (point2.x - point1.x);
float bEdge = (float) (TranslateAniLimited / (Math.sqrt(1 + k * k)));
float aEdge = bEdge * k;
data[0] = bEdge;
data[1] = aEdge;
Log.i("测试", "k=" + k + " x =" + data[0] + " y=" + data[1]);
return data;
}
/**
* 根据target 和touch点计算 control点
* 计算needDrawClosePathDistance
*/
public void computeControl() {
controlPoint.x = (targetPoint.x + touchPoint.x) / 2;
controlPoint.y = (targetPoint.y + touchPoint.y) / 2;
needDrawClosePathDistance = Math.sqrt(Math.pow(targetPoint.x - controlPoint.x, 2) + Math.pow(targetPoint.y - controlPoint.y, 2));
if (needDrawClosePathDistance >= mSplitDistance) {
isSplit = true;
}
if (isSplit) {
return;
}
double R2 = Math.sqrt(Math.pow(targetPoint.x - controlPoint.x, 2) + Math.pow(targetPoint.y - controlPoint.y, 2) - mR * mR);
double[] doubles = new CircleUtil(new Circle(targetPoint.x, targetPoint.y, mR)
, new Circle(controlPoint.x, controlPoint.y, R2)).intersect();
if (doubles != null && doubles.length == 4) {
focusPointA.x = (int) doubles[0];
focusPointA.y = (int) doubles[1];
focusPointB.x = (int) doubles[2];
focusPointB.y = (int) doubles[3];
}
double[] doubles2 = new CircleUtil(new Circle(touchPoint.x, touchPoint.y, mR)
, new Circle(controlPoint.x, controlPoint.y, R2)).intersect();
if (doubles2 != null && doubles2.length == 4) {
focusPointC.x = (int) doubles2[0];
focusPointC.y = (int) doubles2[1];
focusPointD.x = (int) doubles2[2];
focusPointD.y = (int) doubles2[3];
}
}
@Override
public void onAnimationStart(Animation animation) {
isAnimating = true;
}
@Override
public void onAnimationEnd(Animation animation) {
isAnimating = false;
}
@Override
public void onAnimationRepeat(Animation animation) {
}
}
- CircleUtil 2圆相交求交点辅助类
public class CircleUtil {
/**
* 圆A (x-x1)^2 + (y-y1)^2 = r1^2
*/
private Circle c1=null;
/**
* 圆B (x-x2)^2 + (y-y2)^2 = r2^2
*/
private Circle c2=null;
private double x1;
private double y1;
private double x2;
private double y2;
private double r1;
private double r2;
public CircleUtil(Circle C1,Circle C2){
c1= C1;
c2= C2;
x1=c1.getX();
y1=c1.getY();
x2=c2.getX();
y2=c2.getY();
r1=c1.getR();
r2=c2.getR();
}
/**
* 求相交
* @return {x1 , y1 , x2 , y2}
*/
public double[] intersect(){
double a,b,c;
double x_1 = 0,x_2=0,y_1=0,y_2=0;
double delta = -1;
if(y1!=y2){
double A = (x1*x1 - x2*x2 +y1*y1 - y2*y2 + r2*r2 - r1*r1)/(2*(y1-y2));
double B = (x1-x2)/(y1-y2);
a = 1 + B * B;
b = -2 * (x1 + (A-y1)*B);
c = x1*x1 + (A-y1)*(A-y1) - r1*r1;
delta=b*b-4*a*c;
if(delta >0)
{
x_1=(-b+Math.sqrt(b*b-4*a*c))/(2*a);
x_2=(-b-Math.sqrt(b*b-4*a*c))/(2*a);
y_1 = A - B*x_1;
y_2 = A - B*x_2;
}
else if(delta ==0)
{
x_1 = x_2 = -b/(2*a);
y_1 = y_2 = A - B*x_1;
}else
{
System.err.println("两个圆不相交");
return null;
}
}
else if(x1!=x2){
x_1 = x_2 = (x1*x1 - x2*x2 + r2*r2 - r1*r1)/(2*(x1-x2));
a = 1 ;
b = -2*y1;
c = y1*y1 - r1*r1 + (x_1-x1)*(x_1-x1);
delta=b*b-4*a*c;
if(delta >0)
{
y_1 = (-b+Math.sqrt(b*b-4*a*c))/(2*a);
y_2 = (-b-Math.sqrt(b*b-4*a*c))/(2*a);
}
else if(delta ==0)
{
y_1=y_2=-b/(2*a);
}else
{
System.err.println("两个圆不相交");
return null;
}
}
else
{
System.out.println("无解");
return null;
}
return new double[]{x_1,y_1,x_2,y_2};
}
}
- Circle 圆类
public class Circle {
double x;
double y;
double r;
public Circle(double x, double y, double r) {
this.x = x;
this.y = y;
this.r = r;
}
}
- GitHub路径 :https://github.com/yushilei1218/MyApp2.git
记录下供参考~~