【关于Path的史诗级总结】画箭头、直线,监听箭头、直线的触摸事件,以及平移

本文围绕Android开发中Path的使用展开。介绍了Path的绘制与平移,包括直线、箭头、折线等,平移用offset方法;讲解了通过PathMeasure类获取Path上点坐标的方法;还说明了用Region获取填充型Path区域以判断点击事件,线条则用点坐标判断。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

没图我说个球:

1.箭头

2.折线

3.直线

以上只能看到画出来的部分,平移的部分我没移植到这个demo下面(因为遇到了一些bug,懒得搞了),

但是下面会讲到平移部分。

 

 

简介:

该文章将涉及以下知识点:

1.path的绘制与平移(直线、箭头、可弯折直线);

2.path上每个点坐标的获取;

3.使用region获取填充型path(圆、椭圆、实心矩形等)的区域,得到该region后可判断你的path是否被点击;

PS:第3点我特地强调了“填充型path”,因为线条是无法用region获取区域的,获取到的区域永远是空(含泪实测证明)

 

正文:

1.绘制与平移

在主类中重写onTouchEvent方法,根据flag的值判断画哪种类型的view,然后调用该view的setPath方法进行绘制

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = (int) event.getRawX();
                mLastY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_UP:
                mX = (int) event.getX();
                mY = (int) event.getY();
                rawX = (int) event.getRawX();
                rawY = (int) event.getRawY();
                if(!isEnd){
                    switch (flag){
                        case FLAG_DRAW_ARROW:
                            arrowView.setSetTouchArea(true);
                            break;
                        case FLAG_DRAW_LINE:
                            customLine.setSetTouchArea(true);
                            break;
                        case FLAG_DRAW_BENDABLE_LINE:
                            //do nothing
                            break;
                    }
                    flag = FLAG_DRAW_NOTHING;
                    isEnd = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mMoveX = (int) event.getRawX();
                mMoveY = (int) event.getRawY();
                if(!isEnd){
                    switch (flag){
                        case FLAG_DRAW_ARROW:
                            arrowView.clear();
                            arrowView.setPath(mLastX,mLastY,mMoveX,mMoveY);
                            break;
                        case FLAG_DRAW_LINE:
                            customLine.setPath(mLastX,mLastY,mMoveX,mMoveY);
                            break;
                        case FLAG_DRAW_BENDABLE_LINE:
                            bendableLine.setPath(mLastX,mLastY,mMoveX,mMoveY);
                            break;
                    }
                }
                break;
        }

        return true;
    }

直线的setPath方法以及onDraw实现:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    }

    public void setPath(float startX, float startY, float endX, float endY) {
        mPath.reset();
        mPath.moveTo(startX, startY);
        mPath.lineTo(endX, endY);
        invalidate();
    }

箭头的:

@Override
    protected void onDraw(Canvas canvas) {
        if(hasOffset){
            canvas.drawPath(path,paint);
            canvas.drawPath(arrowPath,arrowPaint);
            return;
        }
        setArrowPath();
        canvas.drawPath(path, paint);
        canvas.drawPath(arrowPath, arrowPaint);
    }

/**
     * 画箭头
     */
    public void setArrowPath() {
        double H = 18; // 箭头高度
        double L = 13.5; // 底边的一半

        double angle = Math.atan(L / H); // 箭头角度
        double arrowLength = Math.sqrt(L * L + H * H); // 箭头的长度
        //箭头就是个三角形,我们已经有一个点了,根据箭头的角度和长度,确定另外2个点的位置
        double[] point1 = rotateVec(endX - startX, endY - startY, angle, arrowLength);
        double[] point2 = rotateVec(endX - startX, endY - startY, -angle, arrowLength);
        double point1_x = endX - point1[0];
        double point1_y = endY - point1[1];
        double point2_x = endX - point2[0];
        double point2_y = endY - point2[1];
        int x3 = (int) point1_x;
        int y3 = (int) point1_y;
        int x4 = (int) point2_x;
        int y4 = (int) point2_y;
        // 画线
        arrowPath.moveTo(endX, endY);
        arrowPath.lineTo(x3, y3);
        arrowPath.lineTo(x4, y4);
        arrowPath.close();
    }
    // 计算
    /**
     * @param diffX  X的差值
     * @param diffY  Y的差值
     * @param angle  箭头的角度(箭头三角形的线与直线的角度)
     * @param arrowLength 箭头的长度
     */
    public double[] rotateVec(float diffX, float diffY, double angle, double arrowLength) {
        double arr[] = new double[2];
        // 下面的是公式,得出的是以滑动出的线段末点为中心点旋转angle角度后,线段起点的坐标,这个旋转后的线段也就是“变长了的箭头的三角形的一条边”
        //推导见注释1
        double x = diffX * Math.cos(angle) - diffY * Math.sin(angle);
        double y = diffX * Math.sin(angle) + diffY * Math.cos(angle);
        double d = Math.sqrt(x * x + y * y);
        //根据相似三角形,得出真正的箭头三角形顶点坐标,这里见注释2
        x = x / d * arrowLength;
        y = y / d * arrowLength;
        arr[0] = x;
        arr[1] = y;
        return arr;
    }
    public void setPath(float startX, float startY, float endX, float endY) {
        path.moveTo(startX,startY);
        path.lineTo(endX,endY);
        this.startX = startX;
        this.startY = startY;
        this.endX = endX;
        this.endY = endY;
        invalidate();
    }
    public void clear() {
        path.reset();
        arrowPath.reset();
    }

折线的:

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

        drawPaths(canvas);
    }

    public void setPath(float startX, float startY, final float endX, final float endY) {
        if (pathList.size() <= 0) {
            Path path = new Path();
            path.moveTo(startX, startY);
            path.lineTo(endX, endY);
            lastStartX = startX;
            lastStartY = startY;
            lastEndX = endX;
            lastEndY = endY;
            pathList.add(path);
            currentPath = path;
        } else {
            LogUtil.d(TAG+" lastEndx = "+lastEndX+" , lastEndY = "+lastEndY+" , endX = "+endX+" , endY = "+endY);
            if (Math.abs(lastEndX-endX) <=2 && Math.abs(lastEndY-endY) <= 2) {
                if(isTimerRunning){
                    return;
                }
                //开启悬停计时
                if (mTimer == null) {
                    mTimer = new Timer();
                }
                mTimer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        LogUtil.d(TAG+" do timerTask");
                        Path path = new Path();
                        path.moveTo(lastEndX, lastEndY);
                        path.lineTo(endX, endY);
                        lastStartX = lastEndX;
                        lastStartY = lastEndY;
                        lastEndX = endX;
                        lastEndY = endY;
                        pathList.add(path);
                        currentPath = path;
                        currentPathIndex++;
                        LogUtil.d(TAG + " 生成新直线 : size = " + pathList.size() + " , currentIndex = " + currentPathIndex+" end timer");
                        mHandler.sendEmptyMessage(0);
                        isTimerRunning = false;
                    }
                }, STANDBY_SECONDS);
                isTimerRunning = true;
                LogUtil.d(TAG+" run timer");
            } else {
                currentPath.reset();
                currentPath.moveTo(lastStartX, lastStartY);
                currentPath.lineTo(endX, endY);
                LogUtil.d(TAG + " 更新直线 line to " + endX + ", " + endY);
                lastEndX = endX;
                lastEndY = endY;
                pathList.set(currentPathIndex, currentPath);
            }
        }
        invalidate();
    }

平移:

平移用到的是path自带方法offset(dx,dy),参数就是x轴与y轴的偏移量。

@Override
    public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    if (isContains(event.getRawX(),event.getRawY())) {
                        LogUtil.i(TAG + " touch down id = " + identity);
                        if (onLineTouchListener != null) {
                            onLineTouchListener.onLineTouch(this);
                        }
                        lastX = (int) event.getRawX();
                        lastY = (int) event.getRawY();
                        canMove = true;
                    }else{
                        canMove = false;
                        return false;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if(canMove){
                        int tempRawX = (int) event.getRawX();
                        int tempRawY = (int) event.getRawY();

                        int dx = tempRawX - lastX;
                        int dy = tempRawY - lastY;
                        lastX = tempRawX;
                        lastY = tempRawY;
                        dxTemp += dx;
                        dyTemp += dy;
                        dxTotal += dx;
                        dyTotal += dy;
//                        this.setTranslationX(dxTotal);
//                        this.setTranslationY(dyTotal);
                        mPath.offset(dx,dy);
                        invalidate();
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    if(dxTemp != 0 || dyTemp != 0){
                        setSetTouchArea(true);
                        dxTemp = 0;
                        dyTemp = 0;
                    }
                    break;
        }
        return super.onTouchEvent(event);
    }

 

 

2.path上点坐标的获取

这里我用直线举个栗子,其他path的获取都是一样的方式。

先讲讲原理:

这里用到一个官方提供的类,叫PathMeasure,通过将它与path绑定,能够获取到path的长度,以及path上距离起点distance的点的坐标,然后我们可以写一个循环,获取path上所有点的坐标。

            //用list存放path上的点坐标
            private List<float[]> pathPosList = new ArrayList<>;
            
            //绑定path
            mPathMeasure.setPath(mPath, false);
            /*这里我每次循环完不是i++,而是i+=10,因为如果用i++会得到上千个点坐标,我不需要那么高的精度,这里根据自己需求调整*/
            for (int i = 0; i < mPathMeasure.getLength(); i+=10) {
                float[] pos = new float[2];
                float[] tan = new float[2];
                mPathMeasure.getPosTan(i, pos, tan);
                pathPosList.add(pos);
            }

3.使用Region获取path所占用的区域(对线条无用)

private void getPathRegion(){
        Region globalRegion = new Region(0,0,ScreenUtil.screenWidth,ScreenUtil.screenHeight);
        Region mPathRegion = new Region();
        mPathRegion.setPath(mPath,globalRegion);
    }

代码很简单,解释下原理,region.setPath方法,获取的是传入的path与传入的region的交集。

 

总结:

用path基本想画啥都能画出来,尤其是典型的几何图形,官方都封装好了相应的方法。如果想要做path画出来的非规则图形的点击,填充图形用Region,线条用点坐标来判断。

源码下载地址:https://download.youkuaiyun.com/download/yonghuming_jesse/11164357

以上。

如果有我说的不够详细,你没看懂的地方,请在下方留言,博主会尽快回复。

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JesseAndroid

每一份支持都是我创作的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值