王学岗高级UI6——————贝塞尔曲线

第一,贝塞尔曲线

先看一篇大神的文章

第二,Path的使用,附录一篇文章

path可以理解为路径,它的主要作用是绘制直线,曲线,或者其它的一些几何图形。也可以用于绘制文字。联想canvas.draw()方法,path也是类似的功能。只是canvas比较简单,path绘制出来的图形会更加的复杂。比如我们要绘制五角星,用canvas就很困难了,但是path实现起来却可以很简单。
看下代码,绘制两个简单的圆

package com.example.beisaier;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

/**
 * @author writing
 * @time 2019/12/18 12:34
 * @note
 */
public class PathView extends View {

    private Path path1;
    private Path path2;
    private Paint paint;

    public PathView(Context context) {
        super(context);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        intit();
    }

    private void intit() {
        path1 = new Path();
        path2 = new Path();
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(4);
        paint.setStyle(Paint.Style.STROKE);
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /**
         * Path.Direction
         * Path.Direction.CCW 逆时针
         * Path.Direction.CW 顺时针
         */
        path1.addCircle(200,200,150, Path.Direction.CCW);
        path1.addCircle(300,300,150, Path.Direction.CW);

        canvas.drawPath(path1,paint);
        canvas.drawPath(path2,paint);
    }
}

效果图
在这里插入图片描述
绘制0.x圆

package com.example.beisaier;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

/**
 * @author writing
 * @time 2019/12/18 12:34
 * @note
 */
public class PathView extends View {

    private Path path1;
    private Path path2;
    private Path path3;
    private Paint paint;

    public PathView(Context context) {
        super(context);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        intit();
    }

    private void intit() {
        path1 = new Path();
        path2 = new Path();
        path3 = new Path();
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(4);
        paint.setStyle(Paint.Style.STROKE);
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /**
         * Path.Direction
         * Path.Direction.CCW 逆时针
         * Path.Direction.CW 顺时针
         */
         //半圆
        path1.addArc(200,200,500,500,0,90);
        path2.addArc(200,600,500,900,0,-90);
        canvas.drawPath(path1,paint);
        canvas.drawPath(path2,paint);
        paint.setStyle(Paint.Style.FILL);
        path3.addArc(200,900,500,1200,0,-90);
        canvas.drawPath(path3,paint);
    }
}

在这里插入图片描述

package com.example.beisaier;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

/**
 * @author writing
 * @time 2019/12/18 12:34
 * @note
 */
public class PathView extends View {

    private Path path1;
    private Path path2;
    private Path path3;
    private Paint paint;

    public PathView(Context context) {
        super(context);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        intit();
    }

    private void intit() {
        path1 = new Path();
        path2 = new Path();
        path3 = new Path();
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(4);
        paint.setStyle(Paint.Style.STROKE);
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /**
         * Path.Direction
         * Path.Direction.CCW 逆时针
         * Path.Direction.CW 顺时针
         */
         //0.x椭圆,椭圆的一部分
        path1.arcTo(200, 200, 300, 300, 0, 90, true);
        //椭圆
        path1.addOval(300, 300, 400, 450, Path.Direction.CW);
        //矩形
        path1.addRect(100, 400, 300, 500, Path.Direction.CW);
        //圆角矩形
        path1.addRoundRect(100, 600, 300, 700, 20, 40, Path.Direction.CW);
        canvas.drawPath(path1, paint);
    }
}

在这里插入图片描述
绘制更加复杂的图案

package com.example.beisaier;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

/**
 * @author writing
 * @time 2019/12/18 12:34
 * @note
 */
public class PathView extends View {

    private Path path1;
    private Path path2;
    private Paint paint;

    public PathView(Context context) {
        super(context);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        intit();
    }

    public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        intit();
    }

    private void intit() {
        path1 = new Path();
        path2 = new Path();
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(4);
        paint.setStyle(Paint.Style.STROKE);
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
                /**
         * Path.Direction绘制方向
         * Path.Direction.CCW   逆时针
         * Path.Direction.CW  顺时针
         */
        path1.addCircle(200, 200, 100, Path.Direction.CW); //绘制圆
        path2.addCircle(300, 300, 100, Path.Direction.CW);
        /**
         * Path.op对两个Path进行布尔运算(即取交集、并集等操作)
         * Path.Op.DIFFERENCE 减去path1中path1与path2都存在的部分;
         * path1 = (path1 - path1 ∩ path2)
         * Path.Op.INTERSECT 保留path1与path2共同的部分;
         * path1 = path1 ∩ path2
         * Path.Op.UNION 取path1与path2的并集;
         * path1 = path1 ∪ path2
         * Path.Op.REVERSE_DIFFERENCE 与DIFFERENCE刚好相反;
         * path1 = path2 - (path1 ∩ path2)
         * Path.Op.XOR 与INTERSECT刚好相反;
         * path1 = (path1 ∪ path2) - (path1 ∩ path2)
         */
        //五种情况
//        path1.op(path2,Path.Op.DIFFERENCE);
//        path1.op(path2,Path.Op.INTERSECT);
//        path1.op(path2,Path.Op.UNION);
//        path1.op(path2,Path.Op.XOR);
//        path1.op(path2,Path.Op.REVERSE_DIFFERENCE);
        canvas.drawPath(path1, paint);
        canvas.drawPath(path2, paint);
    }
}

在这里插入图片描述
path的其它一些API

     mPath1.moveTo(100, 100);//将路径的绘制位置定在(x,y)的位置

        /**
         * 在前一个点的基础上开始绘制,如果前面一个点是(x,y),
         * rMoveTo(dx,dy)相当于moveTo(x+dx,y+dy),如果前面没有调用moveTo,
         * 相当于从(dx,dy)开始绘制
         */
        mPath1.rMoveTo(100, 100);

不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)
关于带r的方法与不带r方法的区别,详细请看这篇文章

第三使用Path绘制贝塞尔曲线

path只可以绘制2、3阶贝塞尔曲线。
绘制2阶贝塞尔曲线

   path1.moveTo(100,100);
        path1.quadTo(400,200,10,500);
        canvas.drawPath(path1,paint);

绘制3阶贝塞尔曲线

mPath1.moveTo(100, 100);
        mPath1.cubicTo(400, 200,10, 500,300, 700);
        canvas.drawPath(mPath1, mPaint);

第四绘制n阶贝塞尔曲线

package com.example.beisaier;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * @author writing
 * @time 2019/12/20 14:44
 * @note
 */
public class BezierView extends View {

    private Paint mPaint;
    private Paint mLinePointPaint;
    private Path mPath;
    private List<PointF> mControlPoints;

    public BezierView(Context context) {
        super(context);
        init();
    }

    public BezierView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public BezierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //绘制贝塞尔曲线的画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(10);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.BLACK);
        //绘制点的画笔
        mLinePointPaint = new Paint();
        mLinePointPaint.setAntiAlias(true);
        mLinePointPaint.setStrokeWidth(10);
        mLinePointPaint.setStyle(Paint.Style.STROKE);
        mLinePointPaint.setColor(Color.RED);
        //贝塞尔曲线路径
        mPath = new Path();
        //控制点集合(包含数据点)
        mControlPoints = new ArrayList<>();

        //随机生成控制点,我们这里绘制的是四阶贝塞尔曲线,
        // 如果大家想绘制更高阶的贝塞尔曲线,修改循环的次数就可以

        Random random = new Random();
        for (int i = 0; i < 5; i++) {
            //产生200-1000的随机数
            int x = random.nextInt(800) + 200;
            int y = random.nextInt(800) + 200;
            Log.i("zhang_xin", "随机数x:" + x + ",随机数y:" + y);
            PointF pointF = new PointF(x, y);
            mControlPoints.add(pointF);
        }

        //如果这里大家不想用随机数生成点,可以自己创建点
//        PointF pointF0 = new PointF(200, 200);
//        PointF pointF1 = new PointF(400, 500);
//        PointF pointF2 = new PointF(600, 650);
//        PointF pointF3 = new PointF(800, 500);
//        PointF pointF4 = new PointF(1000, 150);
//        mControlPoints.add(pointF0);
//        mControlPoints.add(pointF1);
//        mControlPoints.add(pointF2);
//        mControlPoints.add(pointF3);
//        mControlPoints.add(pointF4);


    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制起始点、终止点、控制点以及各点的连线
        int size = mControlPoints.size();
        PointF pointF;
        for (int i = 0; i < size; i++) {
            pointF = mControlPoints.get(i);
            if (i > 0) {
                mLinePointPaint.setColor(Color.GREEN);
                canvas.drawLine(mControlPoints.get(i - 1).x, mControlPoints.get(i - 1).y, pointF.x, pointF.y, mLinePointPaint);
            }
            //起点终点换颜色
            if (i == 0) {
                mLinePointPaint.setColor(Color.BLUE);
            } else if (i == size - 1) {
                mLinePointPaint.setColor(Color.BLUE);
            }
            canvas.drawCircle(pointF.x, pointF.y, 20, mLinePointPaint);
        }
        //曲线连接
        buildBezierPoints();

        canvas.drawPath(mPath,mPaint);
    }

    private void buildBezierPoints() {
        mPath.reset();
        ArrayList<PointF> pointFS = new ArrayList<>();
        int order = mControlPoints.size()-1;//贝塞尔的阶数
        //份数,表示一条曲线绘制多少个点,我们这里设置为1000
        float delta = 1.0f / 1000;
        //for循环,求出每一个点
        for (float t = 0; t<=1;t=t+delta) {
            //bezier点集
            PointF pointF = new PointF(deCastelJau(order, 0, t, true), deCastelJau(order, 0, t, false));//计算在曲线上点位置
            pointFS.add(pointF);
            if (pointFS.size() == 1) {
                //如果是起点就移动到起点
                mPath.moveTo(pointFS.get(0).x, pointFS.get(0).y);
            } else {
                //不是起点就连接起来
                mPath.lineTo(pointF.x, pointF.y);
            }
        }
    }
    /**
     * p(i,j) =  (1-t) * p(i-1,j)  +  t * p(i-1,j+1);
     *
     * @param i          阶数
     * @param j          控制点
     * @param t          时间,比值
     * @param calculateX 计算哪个坐标值 true=x
     * @return
     */
    private float deCastelJau(int i, int j, float t, boolean calculateX) {

        if (i == 1) {
            //一阶曲线
            return calculateX ? (1 - t) * mControlPoints.get(j).x + t * mControlPoints.get(j + 1).x :
                    (1 - t) * mControlPoints.get(j).y + t * mControlPoints.get(j + 1).y;
        } else {
            //递归降阶
            return (1 - t) * deCastelJau(i - 1, j, t, calculateX) + t * deCastelJau(i - 1, j + 1, t, calculateX);
        }
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            init();
            invalidate();
        }
        return super.onTouchEvent(event);
    }
}

绘制仿QQ气泡

package com.dn_alan.myapplication;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.PointFEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

/**
 * QQ气泡效果
 */
public class DragBubbleView extends View {

    /**
     * 气泡默认状态--静止
     */
    private final int BUBBLE_STATE_DEFAUL = 0;
    /**
     * 气泡相连
     */
    private final int BUBBLE_STATE_CONNECT = 1;
    /**
     * 气泡分离
     */
    private final int BUBBLE_STATE_APART = 2;
    /**
     * 气泡消失
     */
    private final int BUBBLE_STATE_DISMISS = 3;

    /**
     * 气泡半径
     */
    private float mBubbleRadius;
    /**
     * 气泡颜色
     */
    private int mBubbleColor;
    /**
     * 气泡消息文字
     */
    private String mTextStr;
    /**
     * 气泡消息文字颜色
     */
    private int mTextColor;
    /**
     * 气泡消息文字大小
     */
    private float mTextSize;
    /**
     * 不动气泡的半径
     */
    private float mBubStillRadius;
    /**
     * 可动气泡的半径
     */
    private float mBubMoveableRadius;
    /**
     * 不动气泡的圆心
     */
    private PointF mBubStillCenter;
    /**
     * 可动气泡的圆心
     */
    private PointF mBubMoveableCenter;
    /**
     * 气泡的画笔
     */
    private Paint mBubblePaint;
    /**
     * 贝塞尔曲线path
     */
    private Path mBezierPath;

    private Paint mTextPaint;

    //文本绘制区域
    private Rect mTextRect;

    private Paint mBurstPaint;

    //爆炸绘制区域
    private Rect mBurstRect;

    /**
     * 气泡状态标志
     */
    private int mBubbleState = BUBBLE_STATE_DEFAUL;
    /**
     * 两气泡圆心距离
     */
    private float mDist;
    /**
     * 气泡相连状态最大圆心距离
     */
    private float mMaxDist;
    /**
     * 手指触摸偏移量
     */
    private final float MOVE_OFFSET;

    /**
     *  气泡爆炸的bitmap数组
     */
    private Bitmap[] mBurstBitmapsArray;
    /**
     * 是否在执行气泡爆炸动画
     */
    private boolean mIsBurstAnimStart = false;

    /**
     * 当前气泡爆炸图片index
     */
    private int mCurDrawableIndex;

    /**
     *  气泡爆炸的图片id数组
     */
    private int[] mBurstDrawablesArray = {R.drawable.burst_1, R.drawable.burst_2
            , R.drawable.burst_3, R.drawable.burst_4, R.drawable.burst_5};

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public DragBubbleView(Context context) {
        this(context,null);

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public DragBubbleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr,0
        );

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        //获取 XML layout中的属性值
        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.DragBubbleView,defStyleAttr,0);
        mBubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius,mBubbleRadius);
        mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED);
        mTextStr = array.getString(R.styleable.DragBubbleView_bubble_text);
        mTextSize = array.getDimension(R.styleable.DragBubbleView_bubble_textSize,mTextSize);
        mTextColor = array.getColor(R.styleable.DragBubbleView_bubble_textColor, Color.WHITE);
        //回收TypedArray
        array.recycle();

        mBubStillRadius = mBubbleRadius;
        mBubMoveableRadius = mBubStillRadius;
        mMaxDist = 8 * mBubbleRadius;

        MOVE_OFFSET = mMaxDist / 4;

        //抗锯齿
        mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBubblePaint.setColor(mBubbleColor);
        mBubblePaint.setStyle(Paint.Style.FILL);
        mBezierPath = new Path();

        //文本画笔
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextRect = new Rect();

        //爆炸画笔
        mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBurstPaint.setFilterBitmap(true);
        mBurstRect = new Rect();
        mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length];
        for (int i = 0; i < mBurstDrawablesArray.length; i++) {
            //将气泡爆炸的drawable转为bitmap
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);
            mBurstBitmapsArray[i] = bitmap;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initView(w,h);
    }

    /**
     * 初始化气泡位置
     * @param w
     * @param h
     */
    private void initView(int w, int h) {

        //设置两气泡圆心初始坐标
        if(mBubStillCenter == null){
            mBubStillCenter = new PointF(w / 2,h / 2);
        }else{
            mBubStillCenter.set(w / 2,h / 2);
        }

        if(mBubMoveableCenter == null){
            mBubMoveableCenter = new PointF(w / 2,h / 2);
        }else{
            mBubMoveableCenter.set(w / 2,h / 2);
        }
        mBubbleState = BUBBLE_STATE_DEFAUL;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            {
                if(mBubbleState != BUBBLE_STATE_DISMISS){
//                    将所提供的参数求平方和后开平方根的结果。
                    mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x,
                            event.getY() - mBubStillCenter.y);
                    if(mDist < mBubbleRadius + MOVE_OFFSET){
                        // 加上MOVE_OFFSET是为了方便拖拽
                        mBubbleState = BUBBLE_STATE_CONNECT;
                    }else{
                        mBubbleState = BUBBLE_STATE_DEFAUL;
                    }

                }
            }
            break;

            case MotionEvent.ACTION_MOVE:
            {
                if(mBubbleState != BUBBLE_STATE_DEFAUL){
                    mBubMoveableCenter.x = event.getX();
                    mBubMoveableCenter.y = event.getY();
                    mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x,
                            event.getY() - mBubStillCenter.y);
                    if(mBubbleState == BUBBLE_STATE_CONNECT){
                        // 减去MOVE_OFFSET是为了让不动气泡半径到一个较小值时就直接消失
                        // 或者说是进入分离状态
                        if(mDist < mMaxDist - MOVE_OFFSET){

                            mBubStillRadius = mBubbleRadius - mDist / 8;
                        }else{
                            mBubbleState = BUBBLE_STATE_APART;
                        }
                    }
                    invalidate();
                }
            }
            break;

            case MotionEvent.ACTION_UP:
            {
                if(mBubbleState == BUBBLE_STATE_CONNECT){
                    startBubbleRestAnim();

                }else if(mBubbleState == BUBBLE_STATE_APART){
                    if(mDist < 2 * mBubbleRadius){
                        startBubbleRestAnim();
                    }else{
                        startBubbleBurstAnim();
                    }
                }
            }
            break;
        }
        return true;
    }

    private void startBubbleBurstAnim() {
        //气泡改为消失状态
        mBubbleState = BUBBLE_STATE_DISMISS;
        mIsBurstAnimStart = true;
        //做一个int型属性动画,从0~mBurstDrawablesArray.length结束
        ValueAnimator anim = ValueAnimator.ofInt(0, mBurstDrawablesArray.length);
        anim.setInterpolator(new LinearInterpolator());
        anim.setDuration(500);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //设置当前绘制的爆炸图片index
                mCurDrawableIndex = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //修改动画执行标志
                mIsBurstAnimStart = false;
            }
        });
        anim.start();

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void startBubbleRestAnim() {
        ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
        new PointF(mBubMoveableCenter.x,mBubMoveableCenter.y),
        new PointF(mBubStillCenter.x,mBubStillCenter.y));

        anim.setDuration(200);
        anim.setInterpolator(new OvershootInterpolator(5f));
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mBubMoveableCenter = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mBubbleState = BUBBLE_STATE_DEFAUL;
            }
        });
        anim.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 1、画静止状态
        // 2、画相连状态
        // 3、画分离状态
        // 4、画消失状态---爆炸动画

        // 1、画拖拽的气泡 和 文字,只要不是消失状态都要绘制带文字的圆
        if(mBubbleState != BUBBLE_STATE_DISMISS){
            //静止状态下移动的小圆(没有文字的那个小圆圈)
            canvas.drawCircle(mBubMoveableCenter.x,mBubMoveableCenter.y,
                    mBubMoveableRadius,mBubblePaint);

            mTextPaint.getTextBounds(mTextStr,0,mTextStr.length(),mTextRect);

            canvas.drawText(mTextStr,mBubMoveableCenter.x - mTextRect.width() / 2,
                    mBubMoveableCenter.y + mTextRect.height() / 2,mTextPaint);
        }
        // 2、画相连的气泡状态
        if(mBubbleState == BUBBLE_STATE_CONNECT)
        {
            // 1、画静止气泡
            canvas.drawCircle(mBubStillCenter.x,mBubStillCenter.y,
                    mBubStillRadius,mBubblePaint);
            // 2、画相连曲线
            // 计算控制点坐标,两个圆心的中点
            int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);
            int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);
            //  O1E/O1O2
            float cosTheta = (mBubMoveableCenter.x - mBubStillCenter.x) / mDist;
            // O2E/O1O2
            float sinTheta = (mBubMoveableCenter.y - mBubStillCenter.y) / mDist;
            //求出ABCD四个点的坐标
            float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta;
            float iBubStillStartY = mBubStillCenter.y + mBubStillRadius * cosTheta;
            float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta;
            float iBubStillEndY = mBubStillCenter.y - mBubStillRadius * cosTheta;
            float iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta;
            float iBubMoveableStartY = mBubMoveableCenter.y - mBubMoveableRadius * cosTheta;
            float iBubMoveableEndX = mBubMoveableCenter.x - mBubMoveableRadius * sinTheta;
            float iBubMoveableEndY = mBubMoveableCenter.y + mBubMoveableRadius * cosTheta;



            mBezierPath.reset();//清除Path中的内容, reset不保留内部数据结构(重置路径)

            // 画上半弧
            mBezierPath.moveTo(iBubStillStartX,iBubStillStartY);//将路径的绘制位置定在(x,y)的位置

            mBezierPath.quadTo(iAnchorX,iAnchorY,iBubMoveableEndX,iBubMoveableEndY);//二阶贝塞尔曲线
            // 画下半弧
            mBezierPath.lineTo(iBubMoveableStartX,iBubMoveableStartY);//结束点或者下一次绘制直线路径的开始点

            mBezierPath.quadTo(iAnchorX,iAnchorY,iBubStillEndX,iBubStillEndY);//二阶贝塞尔曲线

            //连接第一个点连接到最后一个点,形成一个闭合区域
            mBezierPath.close();
            canvas.drawPath(mBezierPath,mBubblePaint);
        }

        // 3、画消失状态---爆炸动画
        if(mIsBurstAnimStart){
            mBurstRect.set((int)(mBubMoveableCenter.x - mBubMoveableRadius),
                    (int)(mBubMoveableCenter.y - mBubMoveableRadius),
                    (int)(mBubMoveableCenter.x + mBubMoveableRadius),
                    (int)(mBubMoveableCenter.y + mBubMoveableRadius));

            canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex],null,
                    mBurstRect,mBubblePaint);
        }
    }

    public void reset() {
        initView(getWidth(),getHeight());

        invalidate();
    }
}

package com.dn_alan.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {
    private DragBubbleView dragBubbleView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        //qq气泡
        setContentView(R.layout.activity_main);
        dragBubbleView = findViewById(R.id.drag_buddle_view);
    }

    public void reset(View view) {
        dragBubbleView.reset();
    }
}

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#666666">
    <!--android:clipChildren="false"-->
    <com.dn_alan.myapplication.DragBubbleView
        android:id="@+id/drag_buddle_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:bubble_text="30"
        app:bubble_textColor="#ffffff"
        app:bubble_textSize="12dp"
        app:bubble_radius="12dp"
        app:bubble_color="#ff0000"
        />

    <Button
        android:id="@+id/reset_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="reset"
        android:layout_alignParentBottom="true"
        android:layout_margin="20dp"
        android:textColor="#666666"
        android:text="还原" />

</RelativeLayout>

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="DragBubbleView">
        <attr name="bubble_radius" format="dimension"/>
        <attr name="bubble_color" format="color"/>
        <attr name="bubble_text" format="string"/>
        <attr name="bubble_textSize" format="dimension"/>
        <attr name="bubble_textColor" format="color"/>
    </declare-styleable>
</resources>

看下效果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值