Android 项目:画图白板APP开发(七)——多指画图操作

发布于:2025-09-13 ⋅ 阅读:(13) ⋅ 点赞:(0)

        在之前的系列文章中,我们已经成功构建了一个功能强大的画板核心。然而,到目前为止,我们的画板还只局限于单指操作。本篇,我们将为我们的白板APP注入灵魂,实现流畅的多指操作功能,让我们的应用从“能用”进化到“好用”。

一、触摸事件类型

单点触控相关:这个之前介绍过

  • ACTION_DOWN:当用户首次触摸屏幕时触发,表示触摸的开始。通常是触摸事件的起点。
  • ACTION_MOVE:当用户在屏幕上移动手指时触发,表示触摸点位置发生变化。
  • ACTION_UP:当用户抬起手指时触发,表示触摸结束
  • ACTION_CANCEL:当触摸事件被取消时触发,例如父 View 拦截了事件或系统中止了触摸。

多点触控相关:这个是本章的重点

  • ACTION_POINTER_DOWN:当屏幕上已有触摸点时,另一个手指按下触发,表示新增了一个触摸点
  • ACTION_POINTER_UP:当屏幕上有多个触摸点时,其中一个手指抬起触发,表示某个触摸点结束

二、相关方法

  • event.getActionMasked():返回当前的触摸事件类型(包含单点触控和多点触控)。
  • event.getActionIndex():返回多点触控事件中当前新增触摸点或当前移除触摸点的索引(适用于ACTION_POINTER_DOWN和ACTION_POINTER_UP多点触控相关事件)。
  • getPointerId(int index):根据指定索引返回对应触摸点的唯一ID
  • getX(int index)和getY(int index):返回指定索引的触摸点的X和Y坐标
  • getPointerCount():返回当前触摸点的数量
  • findPointerIndex(int pointerId):根据ID查找触摸点当前索引。

注意:

  • 索引(index):触摸点的编号,从0开始,最大值为getPointerCount()-1。但索引是动态的,会因触摸点的抬起而改变。
  • ID(pointerId):每个触摸点的唯一标识符,在其生命周期内固定不变。因此,ID是跟踪特定触摸点的可靠依据。

例如:一个手指按下,索引为0,ID为0。第二个手指按下,索引为1,ID为1。第一个手指抬起,那么第二个手指的索引变为0,但ID仍为1。

三、多指画图,使用流程

(1)图例

这个是五根手指同时移动画出的结果

(2)准备工作

//定义记录前一个拖动事件发生点的坐标
private float[] mStartXs = new float[20];
private float[] mStartYs = new float[20];
//保存center点
private float[] mCenterXs = new float[20]; //其实这个才是实实在在的起始点
private float[] mCenterYs = new float[20];

private PaintDates[] Paints = new PaintDates[20];
private Path[] paths = new Path[20];

创建支持20个手指同时触控的数据对象。其实之前画笔锋的时候就是在paths[0],Paints[0]中操作的,接下来只需要对其他区域合理管理就可以了。

(3)onTouchEvent

这里直接说明第二根手指的操作周期,主要涉及到 MotionEvent.ACTION_POINTER_DOWNMotionEvent.ACTION_POINTER_UP

1.ACTION_POINTER_DOWN
case MotionEvent.ACTION_POINTER_DOWN:
if(mZoomModel == HUA_HUA){
    if(mModel == EDIT_MODE_PEN){
        actionPointerDown_Pen(event);
    }
}else {
    if(mModel == EDIT_MODE_PEN){
        //这个会有三种情况  :之后再改
        //1.刚刚按下第二个或者距离不够的第二个-->zoom
        //2.按下了距离够了-->single
        //3.漫游松开一个进入拖拽后再按下一个-->zoom
        //假如按下三四个,都遵循前面的手指来

        //首先判断所有的mode的模式
        if(mode == SINGLE){
            //当mode是在画的过程中(距离判断是进入那个模式)
            if(Paints[0].mOnePaths.size()==0){
                mHandler.sendEmptyMessage(102);
                bottomCanvas.drawColor(0,PorterDuff.Mode.CLEAR); //清空一下
                cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);
                actionPointerDown_Zoom(event);
                //这里需要保存一下始终中点
                midPoint(midStartFirst,event);
                //如果没有进入move就进行删除***去up中设置
                isIntoMoveForZoomAndDrag = false;
                mCancelList.add(new MessageStrokes(ZOOM_OPERATION));
                mCancelList.get(mCancelList.size()-1).mainMatrix = new Matrix(mMatrixMain); //保存当时的一个状态
            }else {
                float distance = 0;
                for (int i = 0; i < Paints[0].mOnePaths.size()-1 ; i++) {
                    if(i==0){
                        distance = distanceTo(Paints[0].mx,Paints[0].my,Paints[0].mOnePaths.get(0).x,Paints[0].mOnePaths.get(0).y);
                    }else {
                        distance = distance +distanceTo(Paints[0].mOnePaths.get(i-1).x,Paints[0].mOnePaths.get(i-1).y
                                ,Paints[0].mOnePaths.get(i).x,Paints[0].mOnePaths.get(i).y);
                    }
                }
                if(distance>200f){
                    mode = ALWAYS;
                }else {
                    mHandler.sendEmptyMessage(102);
                    bottomCanvas.drawColor(0,PorterDuff.Mode.CLEAR); //清空一下
                    cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);
                    actionPointerDown_Zoom(event);
                    mCancelList.add(new MessageStrokes(ZOOM_OPERATION));
                    mCancelList.get(mCancelList.size()-1).mainMatrix = new Matrix(mMatrixMain); //保存当时的一个状态
                }
            }
        }else if(mode == DRAG){
            //加一个点就变成zoom
            isChangeDragToZoom = true; //变回了zoom
            cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);
            actionPointerDown_Zoom(event);
            //这个就不能add撤销恢复的类。
        }else if(mode ==ZOOM){
            //当获取到5个点时就开始进入橡皮模式:计算显示图片(这个以后在恢复)
//                            if(event.getPointerCount()==5){
//                                //-------------------------------------------
//                                //将之前的zoom操作保存
//                                for (int i = 0; i <mPaintedList.size() ; i++) {
//                                    mPaintedList.get(i).mMatrixS.add(new Matrix(matrix)) ;
//                                }
//                                //每次使用的时候都保存一下
//                                mMatrixMain.postConcat(matrix);
//                                //保存存完之后应该清空
//                                matrix.reset();
//                                //------------------------------------------------
//
//                                //首先根据5个点求中心点
//                                //1.先求所有的距离
//                                mode = ERASER;
//                                CalculateTenLine(event);
//                                //2.开始画图
//                                mEraser.setVisibility(VISIBLE); //设置为可见
//                                mEraser.setLayoutParams(new AbsoluteLayout.LayoutParams((int) maxDistance,(int) maxDistance
//                                        ,(int) (MaxCenterP.x-maxDistance/2), (int) (MaxCenterP.y-maxDistance/2)));
//                                //move的时候以第五根手指为准(在它没松开之前一直用这个)
//                                relativeOffsetX = MaxCenterP.x-event.getX(0);
//                                relativeOffsetY = MaxCenterP.y-event.getY(0);
//                                paint_eraser.setStrokeWidth(maxDistance);
//                                //----------------------------------
//                                paths[0].reset();
//                                paths[0].moveTo(MaxCenterP.x, MaxCenterP.y);
//                                mStartXs[0] = MaxCenterP.x;
//                                mStartYs[0] = MaxCenterP.y;
//                                //-------------------------------------
//                                Paints[0] = new PaintDates(new Paint(paint_eraser), new ArrayList<>(), MaxCenterP.x, MaxCenterP.y,maxDistance);
//                                mEndWidths[0] = maxDistance;
//                                mCancelList.add(new MessageStrokes(ERASER_STROKE));
//                                mCancelList.get(mCancelList.size()-1).paintStrokes = new ArrayList<>();
//                            }
        }
    }
}

这里只需要注意 actionPointerDown_Pen() 就可以了,后面是放大缩小的内容。

private void actionPointerDown_Pen(MotionEvent event){
    for (int i = 0; i < event.getPointerCount(); i++) {
        int pointerId = event.getPointerId(i);
        if(Paints[pointerId] == null){
            //lastTimes[pointerId] = event.getEventTime();  //当确定是按下的手势时才获取时间
            paths[pointerId].moveTo(event.getX(i),event.getY(i));
            mStartXs[pointerId] = event.getX(i) ;//获取手指落下的x坐标
            mStartYs[pointerId] = event.getY(i);//获取手指落下的y坐标
            Paints[pointerId] = new PaintDates(new Paint(paint_end),new ArrayList<>(),event.getX(i), event.getY(i),mWidth);
            mEndWidths[pointerId] = mWidth;
            mLastWidths[pointerId] = mWidth;
            if(mModel == EDIT_MODE_PEN&&paint_pen.getAlpha()==255){
                mCenterXs[pointerId] = event.getX(i);
                mCenterYs[pointerId] = event.getY(i);
            }
        }
    }
}

这段代码的主要目的是:当有新的手指(非第一根手指)按下屏幕时,为每一根新按下的手指初始化一套独立的绘画数据,以便它们可以同时作画而互不干扰。

流程:

  1. getPointerCount 遍历当前屏幕上所有的手指(指针)

  2. int pointerId = event.getPointerId(i);获取当前遍历到的指针的唯一ID,这个ID在手指按下到抬起期间不会改变,所以使用Pointer ID来关联数据。

  3. 关键判断:检查这个指针ID是否还没有初始化绘画数据

  4. 为该指针创建一个新的 PaintDates 对象(即一笔新笔画)

2.ACTION_POINTER_UP
private void actionPointerUp_PenBF(int pointerId){
    if(Paints[pointerId]!=null){
        if(Paints[pointerId].getLineModel()==LINE&&Paints[pointerId].mOnePaths.size()==0){
            //这种情况就不添加
        }else {
            //临时画一笔需要尽力变形
            if(Paints[pointerId]!=null){
                //将最后的笔宽给加上
                mPaintedList.add(new PaintDates(Paints[pointerId].mPaint,Paints[pointerId].mOnePaths
                        ,Paints[pointerId].mx,Paints[pointerId].my,Paints[pointerId].mWidth));
                mPaintedList.get(mPaintedList.size()-1).setLineModel(Paints[pointerId].getLineModel());
                //mPaintedList.get(mPaintedList.size()-1).draw(cacheCanvas);
                mCancelList.add(new MessageStrokes(NORMAL_ONE_STROKE));
            }
        }

        //归零
        mRectCenterXs[pointerId] = 0f;
        mRectCenterYs[pointerId] = 0f;
    }
}

这段代码的主要目的是:当一根手指抬起时,完成当前笔画的最终处理,将其保存到永久列表中以供重绘,并做好清理工作,为下一次绘画做准备。