Path类封装由直线段,二次曲线和三次曲线组成的复合(多个轮廓)几何路径。 可以使用canvas.drawPath(path,paint)进行填充或描边绘制(基于Paint的Style),也可以用于剪切或在路径上绘制文本。

path常用方法

方法

作用

备注

moveTo

移动起点

移动下一次操作的起点位置

setLastPoint

设置终点

重置当前path中最后一个点位置,如果在绘制之前调用,效果和moveTo相同

lineTo

连接直线

添加上一个点到当前点之间的直线到Path

close

闭合路径

连接第一个点连接到最后一个点,形成一个闭合区域

addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo

添加内容

添加(矩形, 圆角矩形, 椭圆, 圆, 路径, 圆弧) 到当前Path (注意addArc和arcTo的区别)

quadTo, cubicTo

贝塞尔曲线

分别为二阶和三阶贝塞尔曲线

rMoveTo, rLineTo, rQuadTo, rCubicTo

相对方法

不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)

基础方法使用



// 一阶贝塞尔曲线,就是一条直线
// 移动起点到(100, 100)
mPath.moveTo(100, 100);
// 从坐标原点连线到(300, 400)
mPath.lineTo(300, 400);
mPath.lineTo(300, 100);
// 闭合Path
mPath.close();

mPath.moveTo(400,100);
//mPath.lineTo(700,500);
// 等同于lineTo(700,500),相对于前一个点的位置
mPath.rLineTo(300,400);

// 将当点移动到(400,500)
// 相当于mPath.moveTo(400,500);
mPath.rMoveTo(-300,0);
mPath.rLineTo(100,300);
// 改变(500, 800)这个点到(500, 700)
mPath.setLastPoint(500,700);
mPath.lineTo(500,500);
mPath.close();

canvas.drawPath(mPath, mPaint);

注意:
1、moveTo(x,y)是移动到(x,y)。
例如上一个点是(100,100),moveTo(200,200)是将下一次连线的起点修改到(200,200)
2、setLastPoint(x,y)则是将连线的最后一个点修改为(x,y)。
例如如果上一个点是(100,100),当前点是(200,200),当前连线就是(100,100)到(200,200),如果设置setLastPoint(400,400),当前的连线就会变成(100,100)到(400,400)的连线。

  1. 添加基本图形
// 添加图形
// >=21可以使用下面这个方法
//mPath.addArc(100, 100, 500, 500, 30, 140);
// 绘制一段圆弧,开始角度30,扫过140度
mPath.addArc(new RectF(100, 100, 500, 500), 30, 140);
//mPath.close();

// >=21
//mPath.addOval(500, 100, 700, 600, Path.Direction.CW);
// 绘制椭圆Path.Direction.CW顺时针,Path.Direction.CCW逆时针
mPath.addOval(new RectF(500, 100, 700, 600), Path.Direction.CW);

// 绘制圆形
mPath.addCircle(400, 400, 200, Path.Direction.CCW);

// 通过下面的绘制我们会发现,顺时针和逆时针还是会造成很大区别的
// 绘制矩形
//mPath.addRect(400, 600, 800, 900, Path.Direction.CW);
mPath.addRect(400, 600, 800, 900, Path.Direction.CCW);
// 修改最后一个点的位置
mPath.setLastPoint(300, 1000);
//mPath.addRect(new RectF(100, 600, 600, 900), Path.Direction.CW);

// 追加图形
// >=21
//mPath.arcTo(200, 200, 500, 600, 50, 200, false);
// forceMoveTo 为true,绘制时将最后一个点移动到圆弧起点,为false,绘制时,将绘制圆弧之前的最后一个点与圆弧的起点相连
mPath.arcTo(new RectF(200, 600, 600, 900), 50, 150, false);

// 添加一个Path
Path newPath = new Path();
newPath.moveTo(100,500);
newPath.lineTo(700,900);
// 添加Path
mPath.addPath(newPath);

canvas.drawPath(mPath, mPaint);

android drawableLeft用代码设置_贝塞尔曲线

贝塞尔曲线

用一系列点来控制曲线状态的,我们将这些点分为两类:数据点和控制点

mPath.moveTo(200,200);
// 二阶贝塞尔曲线
//mPath.quadTo(200,400,500,500);
// 跟上面这句代码一致,相对位置
mPath.rQuadTo(0,200,300,300);

mPath.moveTo(200,550);
// 三阶贝塞尔曲线
//mPath.cubicTo(200,200,400,800,800,400);
mPath.rCubicTo(0,-350,200,250,600,-150);
  • 一阶贝塞尔曲线
    没有控制点,只有两个数据点A和B,最终结果是一条线段。
  • android drawableLeft用代码设置_连线_02

    android drawableLeft用代码设置_连线_03

  • 计算公式:B(t) = P0 + (P1 - P0)t = (1-t)P0 + tP1
  • 二阶贝塞尔曲线
    由两个数据点A和C,一个控制点B来描述曲线状态
  • android drawableLeft用代码设置_连线_04

    android drawableLeft用代码设置_偏移量_05

  • 三阶贝塞尔曲线
    由两个数据点A和D,两个控制点B和C来描述曲线状态
  • android drawableLeft用代码设置_连线_06

    android drawableLeft用代码设置_Path_07

二阶贝塞尔曲线推导公式

android drawableLeft用代码设置_贝塞尔曲线_08

  • 高阶贝塞尔曲线
    四阶以至于更高的贝塞尔曲线
贝塞尔曲线例子

QQ消息提醒气泡,拖拽回弹,气泡爆炸效果。

  • 如下是需要的坐标的图
public class PathView2 extends View {
    private Context mContext;

    /**
     * 汽包的四个状态
     * 默认,连接,断开,消失
     */
    private enum State {
        BUBBLE_STATE_DEFAULT,
        BUBBLE_STATE_CONNECT,
        BUBBLE_STATE_APART,
        BUBBLE_STATE_DISMISS
    }

    /**
     * 气泡半径
     */
    private float mBubbleRadius;
    /**
     * 气泡颜色
     */
    private int mBubbleColor;
    /**
     * 气泡消息文字
     */
    private String mBubbleText;
    /**
     * 气泡消息文字颜色
     */
    private int mTextColor;
    /**
     * 气泡消息文字大小
     */
    private float mTextSize;


    /**
     * 不动气泡的半径,半径是可变的
     */
    private float mBubFixedRadius;
    /**
     * 不动气泡的圆心
     */
    private PointF mBubFixedCenter;

    /**
     * 可动气泡的半径
     */
    private float mBubMovableRadius;
    /**
     * 可动气泡的圆心
     */
    private PointF mBubMovableCenter;

    /**
     * 气泡的画笔
     */
    private Paint mBubblePaint;
    /**
     * 贝塞尔曲线path
     */
    private Path mBezierPath;
    /**
     * 文字画笔
     */
    private Paint mTextPaint;
    /**
     * 文本绘制区域
     */
    private Rect mTextRect;
    /**
     * 爆炸效果画笔
     */
    private Paint mBurstPaint;
    /**
     * 爆炸效果绘制区域
     */
    private Rect mBurstRect;
    /**
     * 气泡状态标志
     */
    private State mBubbleState = State.BUBBLE_STATE_DEFAULT;
    /**
     * 两气泡圆心距离,可变的
     */
    private float mCenterDist;
    /**
     * 气泡相连状态最大圆心距离
     */
    private float mMaxDist;
    /**
     * 手指触摸偏移量
     */
    private float MOVE_OFFSET;
    /**
     * 气泡爆炸的bitmap数组
     */
    private Bitmap[] mBurstBitmapsArray;
    /**
     * 当前气泡爆炸图片index
     */
    private int mBurstImgIndex;
    /**
     * 气泡爆炸的图片id数组
     */
    private int[] mBurstImgArray;


    public PathView2(Context context) {
        this(context, null);
    }

    public PathView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PathView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;

        init(attrs, defStyleAttr);
    }

    private void init(AttributeSet attrs, int defStyleAttr) {
        TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.PathView2, defStyleAttr, 0);
        mBubbleRadius = typedArray.getDimension(R.styleable.PathView2_bubble_radius, mBubbleRadius);
        mBubbleColor = typedArray.getColor(R.styleable.PathView2_bubble_color, Color.RED);
        mBubbleText = typedArray.getString(R.styleable.PathView2_bubble_text);
        mTextSize = typedArray.getDimension(R.styleable.PathView2_bubble_textSize, mTextSize);
        mTextColor = typedArray.getColor(R.styleable.PathView2_bubble_textColor, Color.WHITE);
        typedArray.recycle();


        // 初始的时候两个圆的半径一致
        mBubFixedRadius = mBubbleRadius;
        mBubMovableRadius = mBubbleRadius;
        // 设置两个圆心最大距离
        mMaxDist = mBubbleRadius * 8;
        // 手指触摸的偏移量
        MOVE_OFFSET = mMaxDist / 4;

        mBurstImgArray = new int[]{
                R.mipmap.burst_1,
                R.mipmap.burst_2,
                R.mipmap.burst_3,
                R.mipmap.burst_4,
                R.mipmap.burst_5
        };

        // 气泡画笔
        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[mBurstImgArray.length];
        for (int i = 0; i < mBurstImgArray.length; i++) {
            // 将气泡爆炸的drawable转为bitmap
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstImgArray[i]);
            mBurstBitmapsArray[i] = bitmap;
        }
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldW, int oldH) {
        super.onSizeChanged(w, h, oldW, oldH);

        // 不动圆气泡圆心,在View的中心
        if (mBubFixedCenter == null) {
            mBubFixedCenter = new PointF(w / 2, h / 2);
        } else {
            mBubFixedCenter.set(new PointF(w / 2, h / 2));
        }

        // 初始的时候可动圆气泡圆心,在View的中心
        if (mBubMovableCenter == null) {
            mBubMovableCenter = new PointF(w / 2, h / 2);
        } else {
            mBubMovableCenter.set(new PointF(w / 2, h / 2));
        }
    }

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

        //1,静止状态,一个气泡加消息数据
        //2, 连接状态,一个气泡加消息数据,贝塞尔曲线,本身位置上气泡,大小可变化
        //3,分离状态,一个气泡加消息数据
        //4,消失状态,爆炸效果

        if (mBubbleState == State.BUBBLE_STATE_CONNECT) {
            // 绘制不动圆
            canvas.drawCircle(mBubFixedCenter.x, mBubFixedCenter.y, mBubbleRadius, mBubblePaint);

            // 计算控制点
            int controlX = (int) ((mBubFixedCenter.x + mBubMovableCenter.x) / 2);
            int controlY = (int) ((mBubFixedCenter.y + mBubMovableCenter.y) / 2);

            // 计算两圆连线与X轴的夹角a,获取他们的正余弦函数值,用来求解A,B,C,D的坐标
            // 从B点画垂直线到X轴与X轴相交于B',B点连接圆心O',因此根据数学知识我们可以推导出,BB'与BO'的夹角就是夹角a的大小
            float sinA = (mBubMovableCenter.y - mBubFixedCenter.y) / mCenterDist;
            float cosA = (mBubMovableCenter.x - mBubFixedCenter.x) / mCenterDist;

            // A 固定圆上的点
            float aX = mBubFixedCenter.x + mBubFixedRadius * sinA;
            float aY = mBubFixedCenter.y - mBubFixedRadius * cosA;
            // B
            float bX = mBubMovableCenter.x + mBubMovableRadius * sinA;
            float bY = mBubMovableCenter.y - mBubMovableRadius * cosA;
            // C
            float cX = mBubMovableCenter.x - mBubMovableRadius * sinA;
            float cY = mBubMovableCenter.y + mBubMovableRadius * cosA;
            // D 固定圆上的点
            float dX = mBubFixedCenter.x - mBubFixedRadius * sinA;
            float dY = mBubFixedCenter.y + mBubFixedRadius * cosA;

            mBezierPath.reset();
            mBezierPath.moveTo(aX, aY);
            mBezierPath.quadTo(controlX, controlY, bX, bY);

            mBezierPath.lineTo(cX, cY);
            mBezierPath.quadTo(controlX, controlY, dX, dY);
            mBezierPath.close();

            canvas.drawPath(mBezierPath, mBubblePaint);
        }

        // 只要不是消失状态都需要绘制文本和移动圆
        if (mBubbleState != State.BUBBLE_STATE_DISMISS) {
            // 绘制移动圆
            canvas.drawCircle(mBubMovableCenter.x, mBubMovableCenter.y, mBubMovableRadius, mBubblePaint);
            // 获取文本占用区域
            mTextPaint.getTextBounds(mBubbleText, 0, mBubbleText.length(), mTextRect);
            // 绘制文本
            canvas.drawText(
                    mBubbleText,
                    mBubMovableCenter.x - (float) (mTextRect.width() / 2),
                    mBubMovableCenter.y + (float) (mTextRect.height() / 2),
                    mTextPaint
            );
        }

        if (mBubbleState == State.BUBBLE_STATE_DISMISS && mBurstImgIndex < mBurstBitmapsArray.length) {
            // 设置爆炸效果绘制区域
            mBurstRect.set(
                    (int) (mBubMovableCenter.x - mBubMovableRadius),
                    (int) (mBubMovableCenter.y - mBubMovableRadius),
                    (int) (mBubMovableCenter.x + mBubMovableRadius),
                    (int) (mBubMovableCenter.y + mBubMovableRadius)
            );
            canvas.drawBitmap(mBurstBitmapsArray[mBurstImgIndex], null, mBurstRect, mBurstPaint);
        }

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mBubbleState != State.BUBBLE_STATE_DISMISS) {
                    // 求圆心距离
                    mCenterDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);

                    // 当小于mBubbleRadius + MOVE_OFFSET,我们认为点到了,否则就没点击到
                    // 加上MOVE_OFFSET增加触摸点面积
                    if (mCenterDist < mBubbleRadius + MOVE_OFFSET) {
                        mBubbleState = State.BUBBLE_STATE_CONNECT;
                    } else {
                        // 因为没有被点击到,所以是默认状态
                        mBubbleState = State.BUBBLE_STATE_DEFAULT;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mBubbleState != State.BUBBLE_STATE_DEFAULT) {
                    // 求圆心距离
                    mCenterDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);
                    // 给移动圆圆心重新赋值
                    mBubMovableCenter.x = event.getX();
                    mBubMovableCenter.y = event.getY();
                    // 判断是否是连接状态
                    if (mBubbleState == State.BUBBLE_STATE_CONNECT) {
                        if (mCenterDist < mMaxDist - MOVE_OFFSET) {
                            // 当拖拽的距离在指定范围内,那么调整不动气泡的半径
                            // 修改固定圆半径,随着圆心距离增加,固定圆半径越来越小
                            mBubFixedRadius = mBubbleRadius - mCenterDist / 8;
                        } else {
                            //当拖拽的距离超过指定范围,那么改成分离状态
                            mBubbleState = State.BUBBLE_STATE_APART;
                        }
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                // 如果连接状态,需要回弹
                if (mBubbleState == State.BUBBLE_STATE_CONNECT) {
                    //橡皮筋动画效果
                    startBubbleRestAnim();
                } else if (mBubbleState == State.BUBBLE_STATE_APART) {
                    // 判断松手的位置
                    if (mCenterDist < mMaxDist) {
                        startBubbleRestAnim();
                    } else {
                        //爆炸效果
                        startBubbleBurstAnim();
                    }
                }
                break;
        }

        return true;
    }

    /**
     * 爆炸动画
     */
    private void startBubbleBurstAnim() {
        mBubbleState = State.BUBBLE_STATE_DISMISS;
        // 设置爆炸动画从0到图片的长度
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mBurstBitmapsArray.length);
        valueAnimator.setDuration(500);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(animation -> {
            // 获取图片的小标
            mBurstImgIndex = (int) animation.getAnimatedValue();
            invalidate();
        });
        valueAnimator.start();
    }

    /**
     * 回弹动画
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void startBubbleRestAnim() {
        ValueAnimator valueAnimator = ValueAnimator.ofObject(
                new PointFEvaluator(),
                new PointF(mBubMovableCenter.x, mBubMovableCenter.y),
                new PointF(mBubFixedCenter.x, mBubFixedCenter.y)
        );
        valueAnimator.setDuration(300);
        valueAnimator.setInterpolator(new OvershootInterpolator(5f));
        valueAnimator.addUpdateListener(animation -> {
            mBubMovableCenter = (PointF) animation.getAnimatedValue();
            invalidate();
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                // 回弹动画执行完,修改当前气泡的状态为默认状态
                mBubbleState = State.BUBBLE_STATE_DEFAULT;
            }
        });
        valueAnimator.start();
    }
}

android drawableLeft用代码设置_连线_09

github上的示例

参考文章

GcsSloop的View系列Path基本操作贝塞尔曲线,百度百科