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