Android 项目:画图白板APP开发(五)——橡皮擦(全面)

在画图白板应用中,橡皮擦是一个必不可少的功能。它让用户能够修正错误,精确调整他们的创作。本文将详细介绍如何在Android画图白板应用中实现一个高效且用户友好的橡皮擦功能。主要分为以下部分:

  • 橡皮擦:使用透明笔迹覆盖
  • 一键清屏:清空画布所有内容
  • 按笔迹清除:清除选中的笔迹
  • 橡皮擦(修改原本 Path 结构):需要更改原笔迹,分割成新笔迹
  • 电子笔笔帽擦除:电子笔笔帽当橡皮擦使用

部分功能演示:

橡皮檫演示效果

一、橡皮擦

1. 使用PorterDuff模式

PorterDuffXfermode是Android中处理图形混合的强大工具。对于橡皮擦,我们可以使用**PorterDuff.Mode.CLEAR**模式,它会将绘制区域的像素完全清除。

2. 代码

java 复制代码
//设置橡皮的属性
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. 代码

java 复制代码
//清空画布
cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);
invalidate();

清空作为Canvas绘制目标的那块BitmapPorterDuff.Mode.CLEAR: 这是核心所在。它是一种混合模式(Blending Mode),其规则是:清除目标图像中的所有像素,忽略源图像。无论原本画布上有什么,这个模式都会将其变为完全透明。

三、按笔迹擦除

1.思路

画一条曲线,清除相交的可视化笔画(橡皮擦笔画除外),下面配置笔迹擦除橡皮。

java 复制代码
//设置笔画删除的属性
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. 代码

当手指在屏幕上移动,显示笔迹擦除橡皮的同时,记录最新的点和上一个点,判断是否跟可视化笔画相交

java 复制代码
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点是重叠的。判断起点是为了排除类似的点。

java 复制代码
//判断每个false的是否相交
private void isCross(float x1, float y1, float x2, float y2) {
    for (int i = 0; i < mPaintedList.size(); i++) {
        //假如为橡皮擦就continue
        if(mPaintedList.get(i).mPaint.getXfermode()==paint_eraser.getXfermode()){
            //如果为橡皮,就pass
            continue;
        }

        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默认为false

                        break;
                    }



                    //当结束的时候
                    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默认为false
                    break;
                }
                //当结束的时候
                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);
}

流程:

  1. 遍历所有可视化线段(排除橡皮擦)
  2. 遍历过程中,先判断线段起点是否跟传入的线段相切。
  3. 接着判断可视化线段每个点组成的线段,是否跟传入的线段相交。
  4. 判断相切或相交了,就将其设置为待删除,同时设置该曲线为高光加粗效果。
  5. 点的逻辑就只需判断相切即可。

注意:xToMatrix 和 yToMatrix 在这里是偏移和放大缩小后的线段点的位置,这个在后续讲解放大缩小时会讲解,各个功能之间相互配合,让程序更出彩。

当手指松开之后,就执行下面的代码。

java 复制代码
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

java 复制代码
}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. 代码

java 复制代码
//用来判断是否相交,如果相交就在的位置断开即可
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()){
                //如果为橡皮,就pass
                break;
            }
            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;
}

逻辑:

  1. 流程跟按笔迹擦除差不多,判断相交之后,保存交点和区域段(需要切割的区域)
  2. move的时候,只需要记录数据。等到手指松开执行 up 时,再去切割所有线段

接下来是松手后执行的代码

java 复制代码
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,不同的的厂商电子笔的数据有所不同,但是使用方式大差不差。当识别到时,更改为上面的介绍的几种橡皮擦即可

java 复制代码
int toolType = event.getToolType(0);
if(toolType == MotionEvent.TOOL_TYPE_ERASER){
//实现逻辑
}
相关推荐
柯南二号3 小时前
【Android】设置让输入框只能输入数字
android
CV资深专家3 小时前
Android 编译系统lunch配置总结
android
2501_915106323 小时前
App Store 软件上架全流程详解,iOS 应用发布步骤、uni-app 打包上传与审核要点完整指南
android·ios·小程序·https·uni-app·iphone·webview
伊织code3 小时前
Matplotlib 2 -绘图、统计、网格、3D
3d·matplotlib·绘图
大菠萝爱上小西瓜4 小时前
分享一篇关于雷电模拟器基于安卓9的安装环境及抓包的详细教程
android
用户2018792831675 小时前
浅析:Synchronized的锁升级机制
android
用户2018792831675 小时前
SystemClock.elapsedRealtime() 和 System.currentTimeMillis()
android
低调小一5 小时前
深入理解 Android targetSdkVersion:从 Google Play 政策到依赖冲突
android
皆过客,揽星河5 小时前
Linux上安装MySQL8详细教程
android·linux·hadoop·mysql·linux安装mysql·数据库安装·详细教程