Android 自定义实现类似QQ消息贝塞尔拖拽效果BezierView

Android 自定义实现类似QQ消息贝塞尔拖拽效果BezierView

有图有真相:

Android 自定义实现类似QQ消息贝塞尔拖拽效果BezierView_第1张图片

思路梳理:

  1. 首先BezierView继承View,文中包含了7个主要的坐标点;
  2. 其中target点是初始化时绘制圆的圆心坐标点;touch点记录用户滑动的坐标点;
  3. 当触发可拖拽状态时,已target点和touch点分别画圆,半径mR在onSizeChanged计算出来。当2圆无交点时, 则需要实时计算2圆中间填充部分;
  4. control点是target点和touch点连线的中间坐标点;也就是2圆中间绘制曲线的控制点;
  5. 4个交点,以control点为中心,对target圆 和touch圆做切线实时计算出来的4个交点,对于贝塞尔公式来说他们都是数据点;
  6. 4个交点通过path采用二次贝塞尔曲线相连并且闭合则达到的样式就是拖拽但未脱离的图形拉伸效果;
  7. 4个交点的计算要使用到圆方程,已control点为中心交于target和touch圆肯定有4个交点,大概是2圆方程求其二元一次方程,二元一次方程与其中一个圆联立求交点;
  8. 当手指释放时,需要判断当前是否处于完全拽开状态,当未处于完全拽开状态则以target与touch点连线方向,反复直线运动几次达到弹动效果;
  9. 最终的效果图如gif;

代码如下:

  1. 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);
        //已视图中心点为target点
        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();//移除动画
                }
                //判断Down点是否在可拖拽范围内
                if (mDragRange.contains((int) x, (int) y)) {
                    //设置状态 并初始touch点
                    isDrag = true;
                    touchPoint.x = (int) x;
                    touchPoint.y = (int) y;
                    computeControl();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //在拖拽的情况下 对touch点 ,及4个交点
                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) {
                    //如果尚未被完全拽开,那么松开时开启一个弹跳的动画
                    //target点与touch点所在的直线方程 计算直线方程,计算直角三角形AB边
                    float[] xyArr = computeRightTriangleAB(targetPoint, touchPoint);
                    if (xyArr != null) {
                        //实现一个沿着 target点与touch点连线方向的直线动画效果
                        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;
        }
        //当处于Drag时 刷新视图即可
        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);

        //3个圆
        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) {

    }
}
  1. 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(){

        // 在一元二次方程中 a*x^2+b*x+c=0
        double a,b,c;

        //x的两个根 x_1 , x_2
        //y的两个根 y_1 , y_2
        double x_1 = 0,x_2=0,y_1=0,y_2=0;

        //判别式的值
        double delta = -1;

        //如果 y1!=y2
        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){

            //当y1=y2时,x的两个解相等
            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};
    }
}
  1. 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;
    }
  }
  1. GitHub路径 :https://github.com/yushilei1218/MyApp2.git

记录下供参考~~

你可能感兴趣的:(android,自定义View,贝塞尔)