Canvas绘制圆滑曲线

   canvas 绘制曲线是一个比较容易实现的逻辑,但是对于签名,或者手写板,大屏会议机等设备原生的Canvas.drawPath(); 要求线条曲率完美,直接使用drawPath显然不能满足需求,这方面的资源网上也不是很多,这里有一点优化心得,记录下来,分享给有需要的伙伴。这里做一个简单的介绍,对于后续细节,需要自己优化。

关于线条优化,需要深入优化线条,可以私信探讨一下

效果图如下

1:蓝色的是原生的drawPath绘制的线条,仔细看的话,里面有很多不圆滑的因素

2:很多转角比较生硬,处理不过好,原因就是这里的点比较密集,抖动较大

3:红色个的线条是优化后的重绘轨迹,明显要圆滑许多。

原理如下

1:直接生成Path,但是不绘制

2:对原有的path进行测量,重新生成等距的point

3:  便利新增点

4:拿到点,进行重新绘制

好了,直接上代码,一目了然,原理是比较简单的,但是自己去琢磨得话,还是挺费时间的。

常规绘制path代码如下

1:在touch_move 得时候调用path.quadTo 方法。绘制二阶贝塞尔曲线,上图,蓝色得线。

2:touch_move 里面有一个getHistroySize 得方法,因为绘制在主线程,线程阻塞,系统touch点无法全部上报,所以我们拿到历史的点,也加入到path里面,这样绘制得线条和手指触摸轨迹贴合比较近

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

    int actionStatues = MotionEvent.ACTION_UP;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int touchX = (int) event.getX();
        int touchY = (int) event.getY();
        actionStatues = event.getAction();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.reset();
                mPath.moveTo(touchX, touchY);
                preXDefault = touchX;
                preYDefault = touchY;
                break;
            case MotionEvent.ACTION_MOVE:
                int hisSize = event.getHistorySize();  //历史点
                for (int hisIndex = 0; hisIndex < hisSize; hisIndex++) {
                    float tempx = event.getHistoricalX(0, hisIndex);
                    float tempy = event.getHistoricalY(0, hisIndex);
                    addPointTpPath(tempx, tempy, event);
                }
                addPointTpPath(touchX, touchY, event);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                addPointTpPath(touchX, touchY, event);
                invalidate();
                break;
        }
        return true;
    }

    private float preXDefault = 0.0f; //原始轨迹
    private float preYDefault = 0.0f; //原始轨迹

    private void addPointTpPath(float touchX, float touchY, MotionEvent event) {
        mPath.quadTo(preXDefault, preYDefault, (touchX + preXDefault) / 2, (touchY + preYDefault) / 2);
        preXDefault = touchX;
        preYDefault = touchY;
    }

3:接下来就开始处理测量重绘步骤,理论上,mPath 只是我们测量得path,不需要把它绘制出来,但是我们需要比对效果,暂时绘制出来

4:先给一个测量生成新得touch点得工具类方法 

package com.wst.pens.ui.util;

import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.util.Log;


import com.wst.pens.ui.MyLog;
import com.ydw.solution2.PathPoint;


import java.io.File;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class BuddleUtil {

    /**
     * @param vertexPointX -- 角度对应顶点X坐标值
     * @param vertexPointY -- 角度对应顶点Y坐标值
     * @param point0X
     * @param point0Y
     * @param point1X
     * @param point1Y
     * @return
     */
    private static double getDegree(double vertexPointX, double vertexPointY, double point0X, double point0Y, double point1X, double point1Y) {
        //向量的点乘
        double vector = (point0X - vertexPointX) * (point1X - vertexPointX) + (point0Y - vertexPointY) * (point1Y - vertexPointY);
        //向量的模乘
        double sqrt = Math.sqrt(
                (Math.abs((point0X - vertexPointX) * (point0X - vertexPointX)) + Math.abs((point0Y - vertexPointY) * (point0Y - vertexPointY)))
                        * (Math.abs((point1X - vertexPointX) * (point1X - vertexPointX)) + Math.abs((point1Y - vertexPointY) * (point1Y - vertexPointY)))
        );
        //反余弦计算弧度
        double radian = Math.acos(vector / sqrt);
        Log.i("====角度::", "==>" + radian);
        //弧度转角度制
        return (180 * radian / Math.PI);
    }


    private static float currentScal = 1.0f;


    /***
     * 上传截图排序
     * @param fileList
     * @return
     */
    public static File[] getFileListBuddle(File[] fileList) {
        File[] fileListCache = fileList;
        if (fileListCache == null || fileListCache.length < 1) {
            return null;
        }
        Arrays.sort(fileList, new Comparator<File>() {

            @Override
            public int compare(File f1, File f2) {
                return f1.compareTo(f2);
            }

        });
        return fileListCache;
    }


    private static void logInfoBuble(String s) {
//        MyLog.bifeng(s);
    }

    private static DecimalFormat decimalFormat;

    public static float floatToFloat(float nums) {
        if (decimalFormat == null) {
            decimalFormat = new DecimalFormat(".0");
        }
        float backNum = 1.0f;
        try {
            backNum = Float.valueOf(decimalFormat.format(new BigDecimal(nums)));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return backNum;
    }

    public static double doubleToTwo(double dou) {
        double backInfo = (double) Math.round(dou * 100) / 100;
        return backInfo;
    }


    public static float floatToFloatTwo(float nums) {
        if (decimalFormat == null) {
            decimalFormat = new DecimalFormat(".00");
        }
        return Float.valueOf(decimalFormat.format(new BigDecimal(nums)));
    }


    /***
     * 计算两点之间的速度
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @param transTime
     * @return
     */
    public static double getLineSpeed(float x1, float y1, float x2, float y2, long transTime) {
        double distanceCache = getLineDistance(x1, y1, x2, y2);
        double speed = doubleToTwo(distanceCache * 10.0 / transTime * 1.0);
        return speed;
    }

    /***
     * 获取两个点的长度
     */
    public static double getLineDistance(float x1, float y1, float x2, float y2) {
        double distanceCache = doubleToTwo(Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)));
        return distanceCache;
    }

    private Path getNewPath(Path pathForm) {
        Path tempPath = new Path();
        PathMeasure pathMes = new PathMeasure(pathForm, false);
        float pointArrPoint[] = {0f, 0f};
        float spValue = 6.0f;
        float lenght = pathMes.getLength();
        int n = (int) (lenght / spValue);
        float curr = 0f;

        pathMes.getPosTan(curr, pointArrPoint, null);
        tempPath.moveTo(pointArrPoint[0], pointArrPoint[1]);
        float temx = pointArrPoint[0];
        float temy = pointArrPoint[1];

        for (int i = 0; i < n; i++) {
            curr = i * spValue;
            pathMes.getPosTan(curr, pointArrPoint, null);
            tempPath.quadTo(
                    temx,
                    temy,
                    (temx + pointArrPoint[0]) / 2,
                    (temy + pointArrPoint[1]) / 2
            );
            temx = pointArrPoint[0];
            temy = pointArrPoint[1];
        }
        MyLog.paintView("touchUp   线条处理  lenght2 " + lenght);
        pathMes.getPosTan(lenght, pointArrPoint, null);
        tempPath.quadTo(
                temx,
                temy,
                pointArrPoint[0],
                pointArrPoint[1]
        );
        return tempPath;
    }

    /****
     *
     * @param pathForm
     * @param jujleSize
     * 缩放渐变参数
     * @return
     */
    public static List<Point> getNewPathList(Path pathForm, float jujleSize) {
        List<Point> pointList = new ArrayList<>();
        PathMeasure pathMes = new PathMeasure(pathForm, false);
        float pointArrPoint[] = {0f, 0f};
        float spValue = jujleSize;
        float lenght = pathMes.getLength();
        int n = (int) (lenght / spValue);
        float curr = 0f;
//        if (touchUp) {
//            pathMes.getPosTan(curr, pointArrPoint, null);
//            pointList.add(new Point((int) pointArrPoint[0], (int) pointArrPoint[1]));
//        }
        for (int i = 0; i < n; i++) {
            curr = i * spValue;
            pathMes.getPosTan(curr, pointArrPoint, null);
            pointList.add(new Point((int) pointArrPoint[0], (int) pointArrPoint[1]));
        }
//        MyLog.paintView("touchUp   线条处理  lenght2 " + lenght);
//        if (touchUp) {
//            pathMes.getPosTan(lenght, pointArrPoint, null);
//            pointList.add(new Point((int) pointArrPoint[0], (int) pointArrPoint[1]));
//        }
        return pointList;
    }

}

5:下面事对新产生得点,做追加处理,然后重新绘制操作,

6:设定两个点间距为40个像素,这个参数根据自己屏幕圆滑程度,修改数值

7:里面有一个currentPath ,这个标识当前移动轨迹得一小段效果。longPath 才是我们最终想要得效果。


    int listSizeNum = 0;
    int lastCX = 0;
    int lastCY = 0;
    float lastControlX = 0;
    float lastControlY = 0;

    List<Point> pointListJujle = new ArrayList<>();  //用来计算最后合成得点

    private float preXDefault = 0.0f; //原始轨迹
    private float preYDefault = 0.0f; //原始轨迹

    private void addPointTpPath(float touchX, float touchY, MotionEvent event) {
        mPath.quadTo(preXDefault, preYDefault, (touchX + preXDefault) / 2, (touchY + preYDefault) / 2);
        preXDefault = touchX;
        preYDefault = touchY;
        float distanceJujleSize = 40.0f;
        List<Point> newPath = BuddleUtil.getNewPathList(mPath, distanceJujleSize);
        if (newPath != null && newPath.size() > 0) {
            int addNum = newPath.size() - listSizeNum;
            if (addNum > 0) {
                MyLog.paintView("===添加点到集合==0000000000000=" + newPath.size() + " / " + addNum);
                for (int i = newPath.size() - addNum; i < newPath.size(); i++) {
                    Point pointadd = newPath.get(i);
                    pointListJujle.add(pointadd);
                    MyLog.paintView("===添加点到集合===" + pointadd);
                    float nextControlX = (lastCX + pointadd.x) / 2.0f;
                    float nextControlY = (lastCY + pointadd.y) / 2.0f;
                    currentPath.reset();
                    currentPath.moveTo(lastControlX, lastControlY);
                    currentPath.quadTo(lastCX, lastCY, nextControlX, nextControlY);
                    lastCX = pointadd.x;
                    lastCY = pointadd.y;
                    lastControlX = nextControlX;
                    lastControlY = nextControlY;
                }
            }
            listSizeNum = newPath.size();
        }
    }

8:抬手得时候,记得把最后一个点和抬手得点做一个计算,不然就会出现绘制少了一段效果,

9:绘制得时候,下面代码只是参考,可以直接绘制出最终效果,这里demo只显示最后效果

绘制代码如下,

  if (actionStatues == MotionEvent.ACTION_UP) {
            mPaint.setColor(Color.RED);
            int lastXCanvas = 0;
            int lastYCanvas = 0;
            for (int i = 0; i < pointListJujle.size(); i++) {
                Point point = pointListJujle.get(i);
                if (i == 0) {
                    currentPathLong.reset();
                    currentPathLong.moveTo(point.x, point.y);
                } else {
                    currentPathLong.quadTo(lastXCanvas, lastYCanvas, (lastXCanvas + point.x) / 2.0f, (lastYCanvas + point.y) / 2.0f);
                }
                lastXCanvas = point.x;
                lastYCanvas = point.y;

            }
            canvas.drawPath(currentPathLong, mPaint);
        }

这样就可以绘制出非常圆滑得线条了,这只是一种线条优化得思路,下面是整体源码

package com.wst.pens.ui;

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

import androidx.annotation.Nullable;

import com.wst.pens.ui.util.BuddleUtil;

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

public class PaintSplitScreenC extends View {

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

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

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

    Path mPath;
    Paint mPaint;

    Path currentPath;
    Path currentPathLong;

    private float PEN_WIDTH_SIZE = 5.0f;

    private void initPaint() {
        mPath = new Path();
        currentPath = new Path();
        currentPathLong = new Path();
        mPaint = new Paint();
        mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);// 设置外边缘
        mPaint.setStrokeWidth(PEN_WIDTH_SIZE);
    }

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

        mPaint.setColor(Color.BLUE);
        canvas.drawPath(mPath, mPaint);

        mPaint.setColor(Color.GRAY);
        canvas.drawPath(currentPath, mPaint);

        if (actionStatues == MotionEvent.ACTION_UP) {
            mPaint.setColor(Color.RED);
            int lastXCanvas = 0;
            int lastYCanvas = 0;
            for (int i = 0; i < pointListJujle.size(); i++) {
                Point point = pointListJujle.get(i);
                if (i == 0) {
                    currentPathLong.reset();
                    currentPathLong.moveTo(point.x, point.y);
                } else {
                    currentPathLong.quadTo(lastXCanvas, lastYCanvas, (lastXCanvas + point.x) / 2.0f, (lastYCanvas + point.y) / 2.0f);
                }
                lastXCanvas = point.x;
                lastYCanvas = point.y;

            }
            canvas.drawPath(currentPathLong, mPaint);
        }
    }

    int actionStatues = MotionEvent.ACTION_UP;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int touchX = (int) event.getX();
        int touchY = (int) event.getY();
        actionStatues = event.getAction();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                pointListJujle.clear();
                pointListJujle.add(new Point(touchX, touchY));
                mPath.reset();
                mPath.moveTo(touchX, touchY);
                currentPath.reset();
                currentPath.moveTo(touchX, touchY);
                listSizeNum = 0;
                preXDefault = touchX;
                preYDefault = touchY;
                lastCX = touchX;
                lastCY = touchY;
                lastControlX = touchX;
                lastControlY = touchY;
                break;
            case MotionEvent.ACTION_MOVE:
                int hisSize = event.getHistorySize();  //历史点
                int pointerCount = event.getPointerCount();
                for (int hisIndex = 0; hisIndex < hisSize; hisIndex++) {
                    for (int finger = 0; finger < pointerCount; finger++) {
                        float tempx = event.getHistoricalX(finger, hisIndex);
                        float tempy = event.getHistoricalY(finger, hisIndex);
                        addPointTpPath(tempx, tempy, event);
                    }
                }
                addPointTpPath(touchX, touchY, event);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                addPointTpPath(touchX, touchY, event);
                invalidate();
                break;
        }
        return true;
    }

    int listSizeNum = 0;
    int lastCX = 0;
    int lastCY = 0;
    float lastControlX = 0;
    float lastControlY = 0;

    List<Point> pointListJujle = new ArrayList<>();  //用来计算最后合成得点

    private float preXDefault = 0.0f; //原始轨迹
    private float preYDefault = 0.0f; //原始轨迹

    private void addPointTpPath(float touchX, float touchY, MotionEvent event) {
        mPath.quadTo(preXDefault, preYDefault, (touchX + preXDefault) / 2, (touchY + preYDefault) / 2);
        preXDefault = touchX;
        preYDefault = touchY;
        float distanceJujleSize = 40.0f;
        List<Point> newPath = BuddleUtil.getNewPathList(mPath, distanceJujleSize);
        if (newPath != null && newPath.size() > 0) {
            int addNum = newPath.size() - listSizeNum;
            if (addNum > 0) {
                MyLog.paintView("===添加点到集合==0000000000000=" + newPath.size() + " / " + addNum);
                for (int i = newPath.size() - addNum; i < newPath.size(); i++) {
                    Point pointadd = newPath.get(i);
                    pointListJujle.add(pointadd);
                    MyLog.paintView("===添加点到集合===" + pointadd);
                    float nextControlX = (lastCX + pointadd.x) / 2.0f;
                    float nextControlY = (lastCY + pointadd.y) / 2.0f;
                    currentPath.reset();
                    currentPath.moveTo(lastControlX, lastControlY);
                    currentPath.quadTo(lastCX, lastCY, nextControlX, nextControlY);
                    lastCX = pointadd.x;
                    lastCY = pointadd.y;
                    lastControlX = nextControlX;
                    lastControlY = nextControlY;
                }
            }
            listSizeNum = newPath.size();
        }
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Canvas绘制曲线可以使用曲线和弧线两种方式。曲线是由一系列的点连接而成,每个点都有相同的曲率。而弧线则是圆周上的一部分。在Canvas中,可以使用moveTo(x, y)和lineTo(x, y)方法来绘制曲线。例如,可以使用以下代码在Canvas绘制一个曲线图形: ```html <canvas width="600" height="300" id="cvs"></canvas> <script> var oCanvas = document.getElementById('cvs'); var ctx = oCanvas.getContext('2d'); ctx.moveTo(10, 100); for(var i = 10; i < 600; i++){ var x = i; var y = 10 * Math.cos(x/10) + 100; ctx.lineTo(x, y); } ctx.strokeStyle = "orangered"; ctx.stroke(); </script> ``` 这段代码使用了Math.cos()函数来计算每个点的y坐标,然后使用lineTo()方法将点连接起来,最后使用stroke()方法绘制曲线。\[1\] 除了使用曲线,还可以使用贝塞尔曲线绘制曲线。贝塞尔曲线是根据四个位置任意的点坐标绘制出的一条光滑曲线。在Canvas中,可以使用bezierCurveTo(dx1, dy1, dx2, dy2, x, y)方法来绘制贝塞尔曲线。例如,可以使用以下代码在Canvas绘制一个贝塞尔曲线: ```html <canvas width="600" height="300" id="cvs"></canvas> <script> var oCanvas = document.getElementById('cvs'); var ctx = oCanvas.getContext('2d'); ctx.beginPath(); ctx.moveTo(50, 150); ctx.bezierCurveTo(100, 50, 200, 200, 250, 100); ctx.strokeStyle = "#FF0000"; ctx.lineWidth = 5; ctx.stroke(); </script> ``` 这段代码使用了bezierCurveTo()方法来绘制贝塞尔曲线,其中的参数分别表示控制点和终点的坐标。\[2\] 综上所述,Canvas可以使用曲线和贝塞尔曲线两种方式来绘制曲线图形。 #### 引用[.reference_title] - *1* *3* [canvas基础入门系列教程(5)-绘制曲线图形](https://blog.csdn.net/keledon/article/details/87114689)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [详述Canvas(四)/绘制曲线](https://blog.csdn.net/rentian1/article/details/78681655)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值