在之前的系列文章中,我们已经成功构建了一个功能强大的画板核心。然而,到目前为止,我们的画板还只局限于单指操作。本篇,我们将为我们的白板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_DOWN 和 MotionEvent.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);
}
}
}
}
这段代码的主要目的是:当有新的手指(非第一根手指)按下屏幕时,为每一根新按下的手指初始化一套独立的绘画数据,以便它们可以同时作画而互不干扰。
流程:
getPointerCount 遍历当前屏幕上所有的手指(指针)
int pointerId = event.getPointerId(i);获取当前遍历到的指针的唯一ID,这个ID在手指按下到抬起期间不会改变,所以使用Pointer ID来关联数据。
关键判断:检查这个指针ID是否还没有初始化绘画数据
为该指针创建一个新的 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;
}
}
这段代码的主要目的是:当一根手指抬起时,完成当前笔画的最终处理,将其保存到永久列表中以供重绘,并做好清理工作,为下一次绘画做准备。