Android 项目:画图白板APP开发(五)——橡皮擦(全面)
在画图白板应用中,橡皮擦是一个必不可少的功能。它让用户能够修正错误,精确调整他们的创作。本文将详细介绍如何在Android画图白板应用中实现一个高效且用户友好的橡皮擦功能。主要分为以下部分:
- 橡皮擦:使用透明笔迹覆盖
- 一键清屏:清空画布所有内容
- 按笔迹清除:清除选中的笔迹
- 橡皮擦(修改原本 Path 结构):需要更改原笔迹,分割成新笔迹
- 电子笔笔帽擦除:电子笔笔帽当橡皮擦使用
部分功能演示:
橡皮檫演示效果
一、橡皮擦
1. 使用PorterDuff模式
PorterDuffXfermode是Android中处理图形混合的强大工具。对于橡皮擦,我们可以使用PorterDuff.Mode.CLEAR
模式,它会将绘制区域的像素完全清除。
2. 代码
//设置橡皮的属性
paint_eraser.setStrokeWidth(50f);
paint_eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
paint_eraser.setStyle(Paint.Style.STROKE);
paint_eraser.setStrokeCap(Paint.Cap.ROUND);
paint_eraser.setStrokeJoin(Paint.Join.ROUND);
paint_eraser.setAntiAlias(true);
paint_eraser.setDither(true);
paint_eraser.setFilterBitmap(true);
paint_eraser.setStrokeMiter(1.0f);
注意:此操作不会修改原始的笔迹数据层 (StrokeManager
)。它只是在视觉上覆盖。
3. 效果图
擦除前:
擦除后:
二、一键清屏
1. 代码
//清空画布
cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);
invalidate();
清空作为Canvas绘制目标的那块Bitmap,PorterDuff.Mode.CLEAR
: 这是核心所在。它是一种混合模式(Blending Mode),其规则是:清除目标图像中的所有像素,忽略源图像。无论原本画布上有什么,这个模式都会将其变为完全透明。
三、按笔迹擦除
1.思路
画一条曲线,清除相交的可视化笔画(橡皮擦笔画除外),下面配置笔迹擦除橡皮。
//设置笔画删除的属性
paint_eraser_sliding.setStrokeWidth(3f);//3
paint_eraser_sliding.setColor(Color.parseColor("#E94F4F"));//设置为红色
paint_eraser_sliding.setStyle(Paint.Style.STROKE);
paint_eraser_sliding.setStrokeCap(Paint.Cap.ROUND);
paint_eraser_sliding.setStrokeJoin(Paint.Join.ROUND);
paint_eraser_sliding.setAntiAlias(true);
paint_eraser_sliding.setDither(true);
paint_eraser_sliding.setFilterBitmap(true);
paint_eraser_sliding.setStrokeMiter(1.0f);
//设置画出来的为虚线
paint_eraser_sliding.setPathEffect( new DashPathEffect(new float[]{20f,10f,5f,10f},0f));
2. 代码
当手指在屏幕上移动,显示笔迹擦除橡皮的同时,记录最新的点和上一个点,判断是否跟可视化笔画相交。
private void actionMove_Sliding(MotionEvent event){//来判断应该删除哪些线条,利用高光显示float cx = (event.getX(0)+ mStartXs[0]) / 2f;float cy = (event.getY(0) + mStartYs[0]) / 2f;mDottedLine.mXY.add(new DottedLineDates.XAndY(event.getX(0),event.getY(0)));mDottedLine.path.quadTo(mStartXs[0], mStartYs[0], cx, cy);//resetDirtyRect(mDottedLine.mRectF,event.getX(0),event.getY(0));//判断是否相交(虚线只用提供两个点即可) isDelete为true时变成高光isCross(mStartXs[0],mStartYs[0],event.getX(0),event.getY(0));mStartXs[0] = event.getX(0);mStartYs[0] = event.getY(0);paths[0].moveTo(cx, cy);cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR); //这个很关键invalidateReason = REASON_DRAW_MOVE;invalidate();
}
可视化笔迹的类型:线和点 ,删除笔迹的标准有所不同
- 线:判断是否相交
- 点:判断是否相切
为了更严谨些,线除了判断相交之外;还需要判断起点和终点是否相切。下面的代码我只写了起点的判断,还有另外原因,是因为用电子笔按下的点,很大可能性是经历了多个move点,这些move点是重叠的。判断起点是为了排除类似的点。
//判断每个false的是否相交
private void isCross(float x1, float y1, float x2, float y2) {for (int i = 0; i < mPaintedList.size(); i++) {//假如为橡皮擦就continueif(mPaintedList.get(i).mPaint.getXfermode()==paint_eraser.getXfermode()){//如果为橡皮,就passcontinue;}boolean isEnding = false;while (!mPaintedList.get(i).isDelete()&&!isEnding){//判断DOTTED_LINE是否为点(在压力发生改变的时候他就会运行到move)所以这个点很难捕捉//假如没有准备删除就遍历(有两种类型的处理:点和线)if(mPaintedList.get(i).getLineModel()==LINE||mPaintedList.get(i).getLineModel()==DOTTED_LINE){//目前是对线的处理//System.out.println("AAAAAAAAAAAAAAAA 进入了while循环:线 id "+i);boolean b1; //对首点的判断float pointWidth = pointToLine(x1,y1,x2,y2,mPaintedList.get(i).mXToMatrix,mPaintedList.get(i).mYToMatrix);b1 = pointWidth<=mPaintedList.get(i).mWidth;for (int j = 0; j < mPaintedList.get(i).mOnePaths.size() ; j++) {boolean b ;if(j==0){//使用最开始保存的点b = intersection(x1,y1,x2,y2,mPaintedList.get(i).mXToMatrix,mPaintedList.get(i).mYToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}else {b = intersection(x1,y1,x2,y2,mPaintedList.get(i).mOnePaths.get(j-1).xToMatrix,mPaintedList.get(i).mOnePaths.get(j-1).yToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}if(b||b1){//如果判断出相交 == 成为待删除//System.out.println("AAAAAAAAAAAAAAAA 查找到了一条可擦线段 id为:"+ i);mPaintedList.get(i).setDelete(true);//同时保存到删除备选
// if(mDeleteList == null){
// mDeleteList = new ArrayList<>();
// }
// mDeleteList.add(new PaintDates(mPaintedList.get(i).mPaint,mPaintedList.get(i).mOnePaths
// ,mPaintedList.get(i).mx,mPaintedList.get(i).my,mPaintedList.get(i).mWidth)); //这里的isDelete默认为falsebreak;}//当结束的时候if(j == (mPaintedList.get(i).mOnePaths.size()- 1)){//结束while循环isEnding=true;}}}else {//对点的处理float pointWidth = pointToLine(x1,y1,x2,y2,mPaintedList.get(i).mXToMatrix,mPaintedList.get(i).mYToMatrix);if(pointWidth<=mPaintedList.get(i).mWidth){//System.out.println("AAAAAAAAAAAAAAAA 查找到了一条可擦点点 id为:"+ i);mPaintedList.get(i).setDelete(true);//同时保存到删除备选
// if(mDeleteList == null){
// mDeleteList = new ArrayList<>();
// }
// mDeleteList.add(new PaintDates(mPaintedList.get(i).mPaint,mPaintedList.get(i).mOnePaths
// ,mPaintedList.get(i).mx,mPaintedList.get(i).my,mPaintedList.get(i).mWidth)); //这里的isDelete默认为falsebreak;}//当结束的时候isEnding=true;}}}
}// 点到直线的最短距离的判断 点(x0,y0) 到由两点组成的线段(x1,y1) ,( x2,y2 )
private float pointToLine(float x1, float y1, float x2, float y2, float x0,float y0) {float space = 0;float a, b, c;a = distanceTo(x1, y1, x2, y2);// 线段的长度b = distanceTo(x1, y1, x0, y0);// (x1,y1)到点的距离c = distanceTo(x2, y2, x0, y0);// (x2,y2)到点的距离if (c <= 0.000001 || b <= 0.000001) {space = 0;return space;}if (a <= 0.000001) {space = b;return space;}if (c * c >= a * a + b * b) {space = b;return space;}if (b * b >= a * a + c * c) {space = c;return space;}float p = (a + b + c) / 2;// 半周长float s = (float) Math.sqrt(p * (p - a) * (p - b) * (p - c));// 海伦公式求面积space = 2 * s / a;// 返回点到线的距离(利用三角形面积公式求高)return space;
}//判断两条直线是否相交
public static boolean intersection(double l1x1, double l1y1, double l1x2, double l1y2,double l2x1, double l2y1, double l2x2, double l2y2)
{// 快速排斥实验 首先判断两条线段在 x 以及 y 坐标的投影是否有重合。 有一个为真,则代表两线段必不可交。if (Math.max(l1x1,l1x2) < Math.min(l2x1 ,l2x2)|| Math.max(l1y1,l1y2) < Math.min(l2y1,l2y2)|| Math.max(l2x1,l2x2) < Math.min(l1x1,l1x2)|| Math.max(l2y1,l2y2) < Math.min(l1y1,l1y2)){return false;}// 跨立实验 如果相交则矢量叉积异号或为零,大于零则不相交return !((((l1x1 - l2x1) * (l2y2 - l2y1) - (l1y1 - l2y1) * (l2x2 - l2x1))* ((l1x2 - l2x1) * (l2y2 - l2y1) - (l1y2 - l2y1) * (l2x2 - l2x1))) > 0)&& !((((l2x1 - l1x1) * (l1y2 - l1y1) - (l2y1 - l1y1) * (l1x2 - l1x1))* ((l2x2 - l1x1) * (l1y2 - l1y1) - (l2y2 - l1y1) * (l1x2 - l1x1))) > 0);
}
流程:
- 遍历所有可视化线段(排除橡皮擦)
- 遍历过程中,先判断线段起点是否跟传入的线段相切。
- 接着判断可视化线段每个点组成的线段,是否跟传入的线段相交。
- 判断相切或相交了,就将其设置为待删除,同时设置该曲线为高光加粗效果。
- 点的逻辑就只需判断相切即可。
注意:xToMatrix 和 yToMatrix 在这里是偏移和放大缩小后的线段点的位置,这个在后续讲解放大缩小时会讲解,各个功能之间相互配合,让程序更出彩。
当手指松开之后,就执行下面的代码。
private void actionUp_Sliding(){//UP开始的时候就开始删除(可能有多个)//1.查找并删除(依据撤销,没必要删除)for (int i = mPaintedList.size()-1 ; i>=0 ; i--) {//反着删除if(mPaintedList.get(i).isDelete()){if(mCancelList.get(mCancelList.size()-1).MassageType==SLIDING_MULTI_STROKE_UN_HAVE){//设置成有效操作mCancelList.get(mCancelList.size()-1).MassageType=SLIDING_MULTI_STROKE_HAVE;mCancelList.get(mCancelList.size()-1).paintStrokes = new ArrayList<>();}mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(i,new PaintDates(mPaintedList.get(i))));mPaintedList.remove(i);}}cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);
}
这里简单点理解就是将 mPaintedList.get(i) 的笔迹移除到了mCancelList.get(mCancelList.size()-1).paintStrokes 中,这里的mCancelList是撤销恢复使用的,在这里无需理会。
注意:这里反向删除,是为了避免以下问题。
当从一个
List
中正向遍历(从0到size-1)并删除元素时,每删除一个元素,后续元素的索引都会前移(即索引值减1),这会导致:
如果删除第i个元素,那么原本第i+1个元素会变成第i个,但循环索引i会继续增加,从而跳过这个元素。
还可能因为索引越界而引发异常(比如删除后列表大小变化,但循环仍试图访问原索引)。
反向遍历(从size-1到0)删除可以避免这个问题:
删除一个元素后,前面元素的索引不会改变(因为删除的是当前索引i,而i是递减的,下一个要检查的是i-1,不受删除影响)。
这样确保每个元素都被正确遍历,且不会出现索引越界。
3. 效果图
选中后的状态,其中红色虚线为擦除橡皮的轨迹,高光的笔迹就为待删除的笔迹
删除结果如下:
四、橡皮擦(修改原本 Path 结构)
上面的效果,就是根据橡皮擦将原本 Path 分割的结果。
1. 思路
当橡皮擦经过 Path 的时候,除了可以遮挡笔迹之外,还希望将 Path 在遮挡的位置断开,形成新的 Path 。下面代码是橡皮擦在Move的时候执行的部分代码,需要留意的是isCross_eraser。
}else if(mode == ERASER){mEraser.setLayoutParams(new AbsoluteLayout.LayoutParams((int)maxDistance,(int)maxDistance,(int) (event.getX(0)+relativeOffsetX-maxDistance/2), (int) (event.getY(0)+relativeOffsetY-maxDistance/2)));Paints[0].setLineModel(DOTTED_LINE);resetDirtyRect(mRectFs[0],mStartXs[0],mStartYs[0],event.getX(0)+relativeOffsetX,event.getY(0)+relativeOffsetY);float cx = (event.getX(0)+relativeOffsetX+mStartXs[0])/2f;float cy = (event.getY(0)+relativeOffsetY+mStartYs[0])/2f;//用来判断是否相交,如果相交就在的位置断开即可isCross_eraser(mStartXs[0],mStartYs[0],event.getX(0)+relativeOffsetX,event.getY(0)+relativeOffsetY);if(Paints[0].mPath == null){Paints[0].mPath = new Path(paths[0]);}Paints[0].mOnePaths.add(new PaintDates.PathAndWidth(event.getX(0)+relativeOffsetX,event.getY(0)+relativeOffsetY));Paints[0].mPath.quadTo(mStartXs[0],mStartYs[0],cx,cy);mStartXs[0] = event.getX(0)+relativeOffsetX;mStartYs[0] = event.getY(0)+relativeOffsetY;
}
2. 代码
//用来判断是否相交,如果相交就在的位置断开即可
private void isCross_eraser(float x1, float y1, float x2, float y2) {PointF pointF;for (int i = 0; i < mPaintedList.size(); i++) {boolean isEnding = false;while (!isEnding){//判断DOTTED_LINE是否为点(在压力发生改变的时候他就会运行到move)所以这个点很难捕捉//假如没有准备删除就遍历(有两种类型的处理:点和线)if(mPaintedList.get(i).mPaint.getXfermode()==paint_eraser.getXfermode()){//如果为橡皮,就passbreak;}if(mPaintedList.get(i).getLineModel()==POINT){break;}if(mPaintedList.get(i).getLineModel()==LINE||mPaintedList.get(i).getLineModel()==DOTTED_LINE){//目前是对线的处理(除开对橡皮的处理)//其实可以两个点之间就设置一个点(并且实时更新就可)for (int j = 0; j < mPaintedList.get(i).mOnePaths.size() ; j++) {boolean b ;if(j==0){b = intersection(x1,y1,x2,y2,mPaintedList.get(i).mXToMatrix,mPaintedList.get(i).mYToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}else {b = intersection(x1,y1,x2,y2,mPaintedList.get(i).mOnePaths.get(j-1).xToMatrix,mPaintedList.get(i).mOnePaths.get(j-1).yToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}if(b){//如果判断出相交 == 成为待删除//System.out.println("AAAAAAAAAAAAAAAA 查找到了一条可擦线段 id为:"+ i);//mPaintedList.get(i).setDelete(true);//设置状态,求比例,保存在两点之间(因为有漫游效果,所以求点是不现实的)//遍历一笔,看到底有几个交点:决定分成多少段if(!mPaintedList.get(i).isCut()){mPaintedList.get(i).setCut(true);}mPaintedList.get(i).mOnePaths.get(j).isCut = true;if(j==0){pointF = intersectionXY(x1,y1,x2,y2,mPaintedList.get(i).mXToMatrix,mPaintedList.get(i).mYToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);//assert pointF != null; //他为空了,中断程序if(pointF!=null){mPaintedList.get(i).mOnePaths.get(j).BL = distanceTo(mPaintedList.get(i).mXToMatrix,mPaintedList.get(i).mYToMatrix,pointF.x,pointF.y)/distanceTo(mPaintedList.get(i).mXToMatrix,mPaintedList.get(i).mYToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}else {mPaintedList.get(i).mOnePaths.get(j).isCut = false; //没有值就设置为不割}}else {pointF = intersectionXY(x1,y1,x2,y2,mPaintedList.get(i).mOnePaths.get(j-1).xToMatrix,mPaintedList.get(i).mOnePaths.get(j-1).yToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);//assert pointF != null;if(pointF!=null){mPaintedList.get(i).mOnePaths.get(j).BL = distanceTo(mPaintedList.get(i).mOnePaths.get(j-1).xToMatrix,mPaintedList.get(i).mOnePaths.get(j-1).yToMatrix,pointF.x,pointF.y)/distanceTo(mPaintedList.get(i).mOnePaths.get(j-1).xToMatrix,mPaintedList.get(i).mOnePaths.get(j-1).yToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}else {mPaintedList.get(i).mOnePaths.get(j).isCut = false; //没有值就设置为不割}//System.out.println("AAAAAAAAAAAAAAAAAAAAAAAA"+pointF.x+","+pointF.y+",j="+j+",bl="+mPaintedList.get(i).mOnePaths.get(j).BL);}}if(j == (mPaintedList.get(i).mOnePaths.size()- 1)){//结束while循环(必须遍历所有,知道结尾)isEnding=true;}}}}}
}//判断交点
public static PointF intersectionXY(float x1, float y1, float x2, float y2,float x3, float y3, float x4, float y4)
{float x;float y;float k1=Float.MAX_VALUE;float k2=Float.MAX_VALUE;boolean flag1=false;boolean flag2=false;if((x1-x2)==0)flag1=true;if((x3-x4)==0)flag2=true;if(!flag1)k1=(y1-y2)/(x1-x2);if(!flag2)k2=(y3-y4)/(x3-x4);if(flag1){x=x1;if(k2==0){y=y3;}else{y=k2*(x-x4)+y4;}}else if(flag2){x=x3;if(k1==0){y=y1;}else{y=k1*(x-x2)+y2;}}else{if(k1==0){y=y1;x=(y-y4)/k2+x4;}else if(k2==0){y=y3;x=(y-y2)/k1+x2;}else{x=(k1*x2-k2*x4+y4-y2)/(k1-k2);y=k1*(x-x2)+y2;}}if(between(x1,x2,x)&&between(y1,y2,y)&&between(x3,x4,x)&&between(y3,y4,y)){PointF point=new PointF();point.x = x;point.y = y;return point;}return null;
}
逻辑:
- 流程跟按笔迹擦除差不多,判断相交之后,保存交点和区域段(需要切割的区域)
- move的时候,只需要记录数据。等到手指松开执行 up 时,再去切割所有线段
接下来是松手后执行的代码
private void actionUp_Eraser(){mEraser.setVisibility(GONE);int size =mPaintedList.size();//这里将其分段(理论上分段的操作应该是不会显示出不同的)for (int i = 0 ; i< size ; i++)for (int i = 0 ; i< size ; i++){if(mPaintedList.get(i).isCut()){ //当此线是准备是剪切的//这里是不透明笔锋if(mPaintedList.get(i).getLineModel()==LINE){mCutList.add(new PaintDatesAndID());//首先要添加一个笔画mCutList.get(mCutList.size()-1).id = i;mCutList.get(mCutList.size()-1).mCutPaintList.add(new PaintDates(mPaintedList.get(i).mPaint,new ArrayList<>(),mPaintedList.get(i).mx,mPaintedList.get(i).my,mPaintedList.get(i).mWidth));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mXToMatrix = mPaintedList.get(i).mXToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mYToMatrix = mPaintedList.get(i).mYToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).setLineModel(mPaintedList.get(i).getLineModel());mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath = mPaintedList.get(i).mPath;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mMatrixS.addAll(mPaintedList.get(i).mMatrixS) ; //这里存疑for (int j = 0; j <mPaintedList.get(i).mOnePaths.size() ; j++) {float cx;float cy;if(mPaintedList.get(i).mOnePaths.get(j).isCut){//根据比例求点PointF pointF ;PointF pointF1; //偏移点if(j==0){pointF= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mx, mPaintedList.get(i).my,mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y);pointF1= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mXToMatrix, mPaintedList.get(i).mYToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}else {pointF= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mOnePaths.get(j-1).x, mPaintedList.get(i).mOnePaths.get(j-1).y,mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y);pointF1= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mOnePaths.get(j-1).xToMatrix, mPaintedList.get(i).mOnePaths.get(j-1).yToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);}mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.add(new PaintDates.PathAndWidth(new Path(mPaintedList.get(i).mOnePaths.get(j).path), mPaintedList.get(i).mOnePaths.get(j).width, pointF.x, pointF.y));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).xToMatrix = pointF1.x;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).yToMatrix = pointF1.y;if(j==0){mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).path.moveTo( mPaintedList.get(i).mx, mPaintedList.get(i).my);cx = (mPaintedList.get(i).mx+ pointF.x)/2f;cy = (mPaintedList.get(i).my+ pointF.y)/2f;}else {mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).path.moveTo( mPaintedList.get(i).mOnePaths.get(j-1).x, mPaintedList.get(i).mOnePaths.get(j-1).y);cx = (mPaintedList.get(i).mOnePaths.get(j-1).x+ pointF.x)/2f;cy = (mPaintedList.get(i).mOnePaths.get(j-1).y+ pointF.y)/2f;}mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).path.lineTo(cx, cy);//new下一个mCutList.get(mCutList.size()-1).mCutPaintList.add(new PaintDates(mPaintedList.get(i).mPaint,new ArrayList<>(),pointF.x,pointF.y,mPaintedList.get(i).mWidth));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mXToMatrix = pointF1.x;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mYToMatrix = pointF1.y;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).setLineModel(mPaintedList.get(i).getLineModel());mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath = mPaintedList.get(i).mPath;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mMatrixS.addAll(mPaintedList.get(i).mMatrixS); //这里存疑mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.add(new PaintDates.PathAndWidth(new Path(mPaintedList.get(i).mOnePaths.get(j).path), mPaintedList.get(i).mOnePaths.get(j).width, mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).path.reset();mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).path.moveTo( pointF.x, pointF.y);mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).path.lineTo(mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y);mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).xToMatrix = mPaintedList.get(i).mOnePaths.get(j).xToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).yToMatrix = mPaintedList.get(i).mOnePaths.get(j).yToMatrix;}else {mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.add(new PaintDates.PathAndWidth(new Path(mPaintedList.get(i).mOnePaths.get(j).path), mPaintedList.get(i).mOnePaths.get(j).width, mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y));if(mPaintedList.get(i).mOnePaths.get(j).addPaths!=null){mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).addPaths = new ArrayList<>(mPaintedList.get(i).mOnePaths.get(j).addPaths) ;}mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).xToMatrix = mPaintedList.get(i).mOnePaths.get(j).xToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).yToMatrix = mPaintedList.get(i).mOnePaths.get(j).yToMatrix;}}}else {//这里是透明笔画(用path的)mCutList.add(new PaintDatesAndID());//首先要添加一个笔画mCutList.get(mCutList.size()-1).mCutPaintList.add(new PaintDates(mPaintedList.get(i).mPaint,new ArrayList<>(),mPaintedList.get(i).mx,mPaintedList.get(i).my,mPaintedList.get(i).mWidth));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mXToMatrix = mPaintedList.get(i).mXToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mYToMatrix = mPaintedList.get(i).mYToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).setLineModel(mPaintedList.get(i).getLineModel());mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath = new Path();mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath.moveTo(mPaintedList.get(i).mx,mPaintedList.get(i).my);mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mMatrixS.addAll(mPaintedList.get(i).mMatrixS) ; //这里存疑for (int j = 0; j <mPaintedList.get(i).mOnePaths.size() ; j++){float cx;float cy;if(mPaintedList.get(i).mOnePaths.get(j).isCut){PointF pointF ;PointF pointF1; //偏移点if(j==0){pointF= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mx, mPaintedList.get(i).my,mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y);pointF1= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mXToMatrix, mPaintedList.get(i).mYToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);cx = (mPaintedList.get(i).mx+pointF.x)/2f;cy = (mPaintedList.get(i).my+pointF.y)/2f;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath.quadTo(mPaintedList.get(i).mx,mPaintedList.get(i).my,cx,cy);}else {pointF= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mOnePaths.get(j-1).x, mPaintedList.get(i).mOnePaths.get(j-1).y,mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y);pointF1= pointToBL(mPaintedList.get(i).mOnePaths.get(j).BL,mPaintedList.get(i).mOnePaths.get(j-1).xToMatrix, mPaintedList.get(i).mOnePaths.get(j-1).yToMatrix,mPaintedList.get(i).mOnePaths.get(j).xToMatrix, mPaintedList.get(i).mOnePaths.get(j).yToMatrix);cx = (mPaintedList.get(i).mOnePaths.get(j-1).x+pointF.x)/2f;cy = (mPaintedList.get(i).mOnePaths.get(j-1).y+pointF.y)/2f;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath.quadTo(mPaintedList.get(i).mOnePaths.get(j-1).x,mPaintedList.get(i).mOnePaths.get(j-1).y,cx,cy);}mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.add(new PaintDates.PathAndWidth(pointF.x, pointF.y));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).xToMatrix = pointF1.x;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).yToMatrix = pointF1.y;//该new一个了mCutList.get(mCutList.size()-1).mCutPaintList.add(new PaintDates(mPaintedList.get(i).mPaint,new ArrayList<>(),pointF.x, pointF.y,mPaintedList.get(i).mWidth));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mXToMatrix = pointF1.x;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mYToMatrix = pointF1.y;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).setLineModel(mPaintedList.get(i).getLineModel());mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath = new Path();mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath.moveTo(pointF.x, pointF.y);mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mMatrixS.addAll(mPaintedList.get(i).mMatrixS) ; //这里存疑cx = (pointF.x+mPaintedList.get(i).mOnePaths.get(j).x)/2f;cy = (pointF.y+mPaintedList.get(i).mOnePaths.get(j).y)/2f;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath.quadTo(pointF.x,pointF.y,cx,cy);mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.add(new PaintDates.PathAndWidth(mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).xToMatrix = mPaintedList.get(i).mOnePaths.get(j).xToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).yToMatrix = mPaintedList.get(i).mOnePaths.get(j).yToMatrix;}else {if(j==0){cx = (mPaintedList.get(i).mx+mPaintedList.get(i).mOnePaths.get(j).x)/2f;cy = (mPaintedList.get(i).my+mPaintedList.get(i).mOnePaths.get(j).y)/2f;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath.quadTo(mPaintedList.get(i).mx,mPaintedList.get(i).my,cx,cy);}else {cx = (mPaintedList.get(i).mOnePaths.get(j-1).x+mPaintedList.get(i).mOnePaths.get(j).x)/2f;cy = (mPaintedList.get(i).mOnePaths.get(j-1).y+mPaintedList.get(i).mOnePaths.get(j).y)/2f;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mPath.quadTo(mPaintedList.get(i).mOnePaths.get(j-1).x,mPaintedList.get(i).mOnePaths.get(j-1).y,cx,cy);}mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.add(new PaintDates.PathAndWidth(mPaintedList.get(i).mOnePaths.get(j).x, mPaintedList.get(i).mOnePaths.get(j).y));mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).xToMatrix = mPaintedList.get(i).mOnePaths.get(j).xToMatrix;mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.get(mCutList.get(mCutList.size()-1).mCutPaintList.get(mCutList.get(mCutList.size()-1).mCutPaintList.size()-1).mOnePaths.size()-1).yToMatrix = mPaintedList.get(i).mOnePaths.get(j).yToMatrix;}}}//在这里保存数字//mNumList.add(mCutList.size());}}if(mCutList.size()!=0){for (int i = mCutList.size()-1; i >= 0; i--) {//从最后一个分割的开始添加,此时包括之前的线+分割线mPaintedList.addAll(mCutList.get(i).id,mCutList.get(i).mCutPaintList);for (int j = 0; j < mCutList.get(i).mCutPaintList.size() ; j++) {mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(mCutList.get(i).id,null));//一定要倒着来}}mCutList.clear();}//将他们delete +mCutList.size()for (int j = mPaintedList.size()-1; j>=0 ; j--) {if(mPaintedList.get(j).isCut()){mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(j,new PaintDates(mPaintedList.get(j))));mPaintedList.remove(j);}}if(Paints[0]!=null){mPaintedList.add(new PaintDates(Paints[0].mPaint,Paints[0].mOnePaths,Paints[0].mx,Paints[0].my,Paints[0].mWidth));mPaintedList.get(mPaintedList.size()-1).setLineModel(Paints[0].getLineModel());mPaintedList.get(mPaintedList.size()-1).mPath = Paints[0].mPath;mPaintedList.get(mPaintedList.size() - 1).draw(cacheCanvas);}paths[0].reset();Paints[0] = null;
}
代码有点多,我简单说说流程:整体分为四个阶
阶段一:遍历并处理被切割的笔画
核心逻辑:正向遍历所有笔画,找到被标记为“切割”(
isCut()
)的笔画进行处理。处理分为两种模式:
1. 不透明笔锋模式 (
LINE
)
创建新的切割记录:在
mCutList
中添加一个PaintDatesAndID
对象。复制原笔画属性:创建一个新的
PaintDates
对象,复制原笔画的 paint、坐标、宽度等基本属性。遍历笔画的每个线段 (
mOnePaths
):
如果线段被切割 (
isCut
):
计算切割点坐标(
pointF
)和对应的矩阵变换点(pointF1
)。将当前线段在切割点处分割:
当前子笔画结束于切割点。
创建一个新的子笔画从切割点开始。
如果线段未被切割:
直接将整个线段添加到当前子笔画中。
2. 透明笔画模式(非
LINE
,使用Path)
逻辑与不透明模式类似,但使用Path和二次贝塞尔曲线(
quadTo
)来构建平滑的笔迹。同样在切割点处分割Path,并创建新的子笔画。
阶段二:将分割后的笔画重新插入原列表
反向遍历
mCutList
(从最后处理的笔画开始)。将分割后产生的子笔画列表(
mCutPaintList
)插入回原笔画列表(mPaintedList
)的原始位置(mCutList.get(i).id
)。为撤销功能记录这些操作(添加到
mCancelList
)。清空临时切割列表
mCutList
。
阶段三:清理被标记为切割的原始笔画
反向遍历当前所有笔画。
找到所有仍被标记为“切割”的原始笔画(这些是刚刚被分割处理的原始笔画)。
将它们从
mPaintedList
中移除,并添加到撤销列表中。
阶段四:处理当前正在绘制的笔画
如果存在正在绘制的笔画(
Paints[0]
),将其完成并添加到笔画列表中。绘制到缓存画布上。
重置路径和笔画变量,为下一次绘制做准备。
五、电子笔笔帽擦除
通过判断是否为MotionEvent.TOOL_TYPE_ERASER,不同的的厂商电子笔的数据有所不同,但是使用方式大差不差。当识别到时,更改为上面的介绍的几种橡皮擦即可。
int toolType = event.getToolType(0);
if(toolType == MotionEvent.TOOL_TYPE_ERASER){
//实现逻辑
}