Android自定义View之Path基础(一)

本文深入解析了Path类的功能与使用方法,包括移动起点、连接直线、闭合路径、添加图形等内容。同时,详细介绍了贝塞尔曲线的原理与实例,探讨了一阶、二阶、三阶贝塞尔曲线的计算公式及其在实际场景中的应用,如QQ消息提醒气泡效果。

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);

在这里插入图片描述

  1. 贝塞尔曲线

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

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,最终结果是一条线段。
    在这里插入图片描述在这里插入图片描述
    计算公式:B(t) = P0 + (P1 - P0)t = (1-t)P0 + tP1

  • 二阶贝塞尔曲线
    由两个数据点A和C,一个控制点B来描述曲线状态
    在这里插入图片描述在这里插入图片描述

  • 三阶贝塞尔曲线
    由两个数据点A和D,两个控制点B和C来描述曲线状态
    在这里插入图片描述在这里插入图片描述

二阶贝塞尔曲线推导公式
推导公式

  • 高阶贝塞尔曲线
    四阶以至于更高的贝塞尔曲线
    在这里插入图片描述
贝塞尔曲线例子

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();
    }
}

在这里插入图片描述

github上的示例

参考文章

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃骨头不吐股骨头皮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值