Android 项目:画图白板APP开发(九)——撤销、恢复(覆盖前文所有功能)
本节为我们的白板APP实现撤销(Undo)和恢复(Redo)功能。这节跟之前实现的功能很多都挂钩,接下来看下涉及到的历史记录操作。
private final int NORMAL_ONE_STROKE = 1; //正常笔画
private final int SLIDING_MULTI_STROKE_HAVE = 2; //滑动橡皮,删除了笔画
private final int SLIDING_MULTI_STROKE_UN_HAVE = 3; //滑动橡皮,形成无效操作
private final int ERASER_STROKE = 4;//橡皮
private final int ZOOM_OPERATION = 5;//缩放操作
private final int CLEAT_SCREEN_OPERATION = 6;//清屏操作
NORMAL_ONE_STROKE:单指和多指的笔画标记
SLIDING_MULTI_STROKE_HAVE:滑动橡皮操作删除了一条或多条笔画
SLIDING_MULTI_STROKE_UN_HAVE:滑动橡皮执行无效操作(没有删除笔画)
ERASER_STROKE:普通橡皮擦操作
ZOOM_OPERATION:放大缩小操作
CLEAT_SCREEN_OPERATION:执行清屏操作
private ArrayList<MessageStrokes> mCancelList; //全局撤销
private ArrayList<MessageStrokes> mRecoverList; //全局恢复
一、流程实现(准备工作)
(1)如何使用
当用户执行完操作,即将开始绘图的时候(手指抬起,按钮被点击),将对应的消息信息保存到 MessageStrokes 中,塞入 mCancelList 当中等待被撤销。
当执行撤销后,再将对应的 MessageStrokes 塞入mRecoverList 等待恢复。
(2)MessageStrokes
将操作的信息保存到MessageStrokes中。
//负责保存每一个操作
public class MessageStrokes {int MassageType; //信息种类ArrayList<IdAndStrokes> paintStrokes;//保存每个笔画的Matrix matrix;Matrix mainMatrix;//用于保存右侧的数字public MessageStrokes(int massageType) {MassageType = massageType;}static class IdAndStrokes{int id ;int num ;//针对于橡皮擦单独设置,用来判断需要删除此ID几次。PaintDates pd ;public IdAndStrokes(int id,PaintDates pd) {this.id = id;this.pd = pd;}}
}
paintStrokes:保存被改变的笔画(被滑动删除的,被橡皮分割的)和其对应的位置id
matrix:撤销时保存之前的状态
mainMatrix:保存操作之前的比例状态
(3)每个功能的具体实现
- NORMAL_ONE_STROKE:在手指抬起的时候将 MessageStrokes 添加到 mCancelList,单指和多指是一个道理,已松手的节点为主。
- SLIDING_MULTI_STROKE_HAVE和SLIDING_MULTI_STROKE_UN_HAVE:在滑动橡皮按下的时候创建一个信息为 SLIDING_MULTI_STROKE_UN_HAVE 的 MessageStrokes,后续手指抬起的时候再判断是否为有效操作,如果为有效操作,将信息改为SLIDING_MULTI_STROKE_HAVE 并保存被删除的笔画。
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);
- ERASER_STROKE:在按下时创建一个信息为 ERASER_STROKE的 MessageStrokes,松开时如果有被分割的,需要保存分割后新增笔画的位置ID ,也需要保存被分割笔画的位置ID和笔画
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++) {//只保存id,等删除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);}
}
- ZOOM_OPERATION:在确定进入缩放模式时,创建一个信息为 ZOOM_OPERATION的 MessageStrokes,并且保存当期的比例状态到 mainMatrix 。
mCancelList.add(new MessageStrokes(ZOOM_OPERATION));
mCancelList.get(mCancelList.size()-1).mainMatrix = new Matrix(mMatrixMain); //保存当时的一个状态
- CLEAT_SCREEN_OPERATION:执行清空的时候,创建对应消息的 MessageStrokes 保存所有笔画即可。
//清空方法2(完全清空)
public void clear(){if(mPaintedList.size()>0){//需要将mPaintedList全部保存到mCancelList中mCancelList.add(new MessageStrokes(CLEAT_SCREEN_OPERATION));mCancelList.get(mCancelList.size()-1).paintStrokes = new ArrayList<>();for (int i = 0; i < mPaintedList.size(); i++) {mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(i,new PaintDates(mPaintedList.get(i))));}mPaintedList.clear();//如果clear的话mRecoverList.clear();cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);bottomCanvas.drawColor(0,PorterDuff.Mode.CLEAR);invalidateReason = REASON_RE;invalidate();mHandler.sendEmptyMessage(102);}
}
二、执行撤销恢复功能
//撤销
public void revoked(){if(mCancelList.size()>0){reDraw(mCancelList,mPaintedList,REVOKE);}
}
//恢复
public void unRevoked(){if(mRecoverList.size()>0){reDraw(mRecoverList,mPaintedList,UN_REVOKE);}
}
主要还是得看下reDraw方法的实现。
private void reDraw(ArrayList<MessageStrokes> List, ArrayList<PaintDates> pl, int type) {if(type == REVOKE){if(List.get(List.size()-1).MassageType==CLEAT_SCREEN_OPERATION){//恢复清屏for (int i = 0; i < List.get(List.size()-1).paintStrokes.size(); i++) {pl.add(new PaintDates(List.get(List.size()-1).paintStrokes.get(i).pd));}List.remove(List.size()-1);//移除尾部//在RecoverList中+1mRecoverList.add(new MessageStrokes(CLEAT_SCREEN_OPERATION));}else if(List.get(List.size()-1).MassageType==NORMAL_ONE_STROKE){//单笔画mRecoverList.add(new MessageStrokes(NORMAL_ONE_STROKE));mRecoverList.get(mRecoverList.size()-1).paintStrokes = new ArrayList<>();mRecoverList.get(mRecoverList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(-1,new PaintDates(pl.get(pl.size()-1))));pl.remove(pl.size()-1);List.remove(List.size()-1);}else if(List.get(List.size()-1).MassageType==ZOOM_OPERATION){//漫游//此时需要传输一个消息mRecoverList.add(new MessageStrokes(ZOOM_OPERATION));mRecoverList.get(mRecoverList.size()-1).mainMatrix = new Matrix(mMatrixMain); //为恢复做准备mMatrixMain = List.get(List.size()-1).mainMatrix;mMatrixMain.getValues(mainDate);nowBL = mainDate[0];Message m = this.handler.obtainMessage();m.what = 0x103;m.obj = nowBL;this.handler.sendMessage(m);if(pl.size()==0){List.remove(List.size()-1);return; //加入空操纵就退出}mRecoverList.get(mRecoverList.size()-1).matrix = new Matrix(pl.get(0).mMatrixS.get(pl.get(0).mMatrixS.size()-1));for (int i = 0; i <pl.size() ; i++) {pl.get(i).mMatrixS.remove(pl.get(i).mMatrixS.size()-1);}updatePoints();List.remove(List.size()-1);}else if(List.get(List.size()-1).MassageType==SLIDING_MULTI_STROKE_HAVE){//笔画删除并有效mRecoverList.add(new MessageStrokes(SLIDING_MULTI_STROKE_HAVE));mRecoverList.get(mRecoverList.size()-1).paintStrokes = new ArrayList<>();for (int i = List.get(List.size()-1).paintStrokes.size()-1; i >=0; i--) {pl.add(List.get(List.size()-1).paintStrokes.get(i).id,new PaintDates(List.get(List.size()-1).paintStrokes.get(i).pd));mRecoverList.get(mRecoverList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(List.get(List.size()-1).paintStrokes.get(i).id,null));}List.remove(List.size()-1);}else if(List.get(List.size()-1).MassageType==SLIDING_MULTI_STROKE_UN_HAVE){//笔画删除无效List.remove(List.size()-1);reDraw(mCancelList,mPaintedList,REVOKE);//接着调用自身return;}else if(List.get(List.size()-1).MassageType==ERASER_STROKE){//橡皮擦mRecoverList.add(new MessageStrokes(ERASER_STROKE));mRecoverList.get(mRecoverList.size()-1).paintStrokes = new ArrayList<>();//第一步:删除橡皮笔画mRecoverList.get(mRecoverList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(pl.size()-1,new PaintDates(pl.get(pl.size()-1))));pl.remove(pl.size()-1);//第二步:倒着添加完整的笔画//第三步:倒着删除新增的笔画for (int i = List.get(List.size()-1).paintStrokes.size()-1; i >=0 ; i--) {if(List.get(List.size()-1).paintStrokes.get(i).pd != null){//完整笔画mRecoverList.get(mRecoverList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(List.get(List.size()-1).paintStrokes.get(i).id,null));pl.add(List.get(List.size()-1).paintStrokes.get(i).id,new PaintDates(List.get(List.size()-1).paintStrokes.get(i).pd));}else {//半头笔画mRecoverList.get(mRecoverList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(List.get(List.size()-1).paintStrokes.get(i).id,new PaintDates(pl.remove(List.get(List.size()-1).paintStrokes.get(i).id))));}}List.remove(List.size()-1);}}else {if(List.get(List.size()-1).MassageType==CLEAT_SCREEN_OPERATION){//恢复撤销mCancelList.add(new MessageStrokes(CLEAT_SCREEN_OPERATION));mCancelList.get(mCancelList.size()-1).paintStrokes = new ArrayList<>();for (int i = 0; i < pl.size(); i++) {mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(i,new PaintDates(pl.get(i))));}pl.clear();List.remove(List.size()-1);}else if(List.get(List.size()-1).MassageType==NORMAL_ONE_STROKE){//单笔画mCancelList.add(new MessageStrokes(NORMAL_ONE_STROKE));pl.add(new PaintDates(List.get(List.size()-1).paintStrokes.get(List.get(List.size()-1).paintStrokes.size()-1).pd));List.remove(List.size()-1);}else if(List.get(List.size()-1).MassageType==ZOOM_OPERATION){//漫游mCancelList.add(new MessageStrokes(ZOOM_OPERATION));mCancelList.get(mCancelList.size()-1).mainMatrix = new Matrix(mMatrixMain);mMatrixMain = List.get(List.size()-1).mainMatrix;mMatrixMain.getValues(mainDate);nowBL = mainDate[0];Message m = this.handler.obtainMessage();m.what = 0x103;m.obj = nowBL;this.handler.sendMessage(m);for (int i = 0; i <pl.size() ; i++) {pl.get(i).mMatrixS.add(List.get(List.size()-1).matrix);}updatePoints();List.remove(List.size()-1);}else if(List.get(List.size()-1).MassageType==SLIDING_MULTI_STROKE_HAVE){//笔画删除并有效mCancelList.add(new MessageStrokes(SLIDING_MULTI_STROKE_HAVE));mCancelList.get(mCancelList.size()-1).paintStrokes = new ArrayList<>();for (int i = List.get(List.size()-1).paintStrokes.size()-1; i >=0; i--) {mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(List.get(List.size()-1).paintStrokes.get(i).id,new PaintDates(pl.remove(List.get(List.size()-1).paintStrokes.get(i).id))));}List.remove(List.size()-1);}else if(List.get(List.size()-1).MassageType==ERASER_STROKE){//橡皮擦mCancelList.add(new MessageStrokes(ERASER_STROKE));mCancelList.get(mCancelList.size()-1).paintStrokes = new ArrayList<>();//第一步:添加新增的笔画//第二步:删除完整笔画for (int i = List.get(List.size()-1).paintStrokes.size()-1; i > 0 ; i--) {if(List.get(List.size()-1).paintStrokes.get(i).pd != null){//半头笔画mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(List.get(List.size()-1).paintStrokes.get(i).id,null));pl.add(List.get(List.size()-1).paintStrokes.get(i).id,new PaintDates(List.get(List.size()-1).paintStrokes.get(i).pd));}else {//完整笔画mCancelList.get(mCancelList.size()-1).paintStrokes.add(new MessageStrokes.IdAndStrokes(List.get(List.size()-1).paintStrokes.get(i).id,new PaintDates(pl.remove(List.get(List.size()-1).paintStrokes.get(i).id))));}}//第三步:添加橡皮笔画pl.add(new PaintDates(List.get(List.size()-1).paintStrokes.get(0).pd));List.remove(List.size()-1);}}cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);invalidateReason = REASON_RE;invalidate();
}
(1)当 type == REVOKE
(撤销操作)
撤销是从 mCancelList
中取出最新的一条命令,执行它的逆操作,并将其移动到 mRecoverList
。
CLEAT_SCREEN_OPERATION
(清屏操作)逆操作:恢复被清屏的所有笔画。
逻辑:从命令中取出之前保存的所有笔画 (
paintStrokes
),重新添加到当前绘制列表 (pl
) 的末尾。结果:画面恢复到清屏之前的状态。
NORMAL_ONE_STROKE
(普通单笔画)逆操作:删除最后画的那一笔。
逻辑:将当前绘制列表 (
pl
) 的最后一个元素(即最新画的一笔)删除,并创建一个新的撤销命令(包含这笔的数据)存入mRecoverList
。结果:画布上最后一笔消失。
ZOOM_OPERATION
(缩放操作)逆操作:恢复视图的变换矩阵到上一次的状态。
逻辑:
保存当前矩阵到
mRecoverList
为恢复做准备。将画布的主变换矩阵 (
mMatrixMain
) 设置为命令中保存的旧矩阵。更新UI显示比例 (
nowBL
)。移除当前每个笔画数据中记录的最近一次变换矩阵(因为这次变换被撤销了)。
调用
updatePoints()
可能用于更新笔画坐标以适应新的矩阵。
结果:画布的缩放和平移状态回退一步。
SLIDING_MULTI_STROKE_HAVE
&UN_HAVE
(滑动删除-有效/无效)HAVE
的逆操作:恢复被删除的多条笔画。逻辑:根据命令中记录的笔画ID和数据,将笔画重新插入到当前绘制列表 (
pl
) 的原始位置(pl.add(id, element)
)。UN_HAVE
的逻辑:这是一个空操作或无效操作,直接移除它,然后递归调用reDraw
处理下一个有效命令。
ERASER_STROKE
(橡皮擦操作)逆操作:恢复被擦除的笔画片段,并移除橡皮擦轨迹。
逻辑:这是最复杂的操作,分三步完美逆操作:
第一步:移除代表橡皮擦路径的笔画(
pl.remove(pl.size()-1)
)。第二步和第三步:处理被擦断的笔画。命令中既保存了完整的原始笔画(用于恢复被完全擦除的部分),也保存了被擦后的剩余笔画(半头笔画)。撤销时,需要删除剩余笔画,并添加回原始完整笔画。
结果:被擦掉的内容重新出现,橡皮擦的痕迹消失。
(2)当 type != REVOKE
(恢复操作)
恢复是撤销的逆过程。从 mRecoverList
中取出命令,重新执行它,并将其移动到 mCancelList
。其逻辑与撤销相对应。