当前位置: 首页 > news >正文

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

  1. CLEAT_SCREEN_OPERATION (清屏操作)

    • 逆操作:恢复被清屏的所有笔画。

    • 逻辑:从命令中取出之前保存的所有笔画 (paintStrokes),重新添加到当前绘制列表 (pl) 的末尾。

    • 结果:画面恢复到清屏之前的状态。

  2. NORMAL_ONE_STROKE (普通单笔画)

    • 逆操作:删除最后画的那一笔。

    • 逻辑:将当前绘制列表 (pl) 的最后一个元素(即最新画的一笔)删除,并创建一个新的撤销命令(包含这笔的数据)存入 mRecoverList

    • 结果:画布上最后一笔消失。

  3. ZOOM_OPERATION (缩放操作)

    • 逆操作:恢复视图的变换矩阵到上一次的状态。

    • 逻辑

      • 保存当前矩阵到 mRecoverList 为恢复做准备。

      • 将画布的主变换矩阵 (mMatrixMain) 设置为命令中保存的旧矩阵。

      • 更新UI显示比例 (nowBL)。

      • 移除当前每个笔画数据中记录的最近一次变换矩阵(因为这次变换被撤销了)。

      • 调用 updatePoints() 可能用于更新笔画坐标以适应新的矩阵。

    • 结果:画布的缩放和平移状态回退一步。

  4. SLIDING_MULTI_STROKE_HAVE & UN_HAVE (滑动删除-有效/无效)

    • HAVE的逆操作:恢复被删除的多条笔画。

    • 逻辑:根据命令中记录的笔画ID和数据,将笔画重新插入到当前绘制列表 (pl) 的原始位置pl.add(id, element))。

    • UN_HAVE的逻辑:这是一个空操作或无效操作,直接移除它,然后递归调用 reDraw 处理下一个有效命令。

  5. ERASER_STROKE (橡皮擦操作)

    • 逆操作:恢复被擦除的笔画片段,并移除橡皮擦轨迹。

    • 逻辑:这是最复杂的操作,分三步完美逆操作:

      • 第一步:移除代表橡皮擦路径的笔画(pl.remove(pl.size()-1))。

      • 第二步和第三步:处理被擦断的笔画。命令中既保存了完整的原始笔画(用于恢复被完全擦除的部分),也保存了被擦后的剩余笔画(半头笔画)。撤销时,需要删除剩余笔画,并添加回原始完整笔画。

    • 结果:被擦掉的内容重新出现,橡皮擦的痕迹消失。

(2)当 type != REVOKE (恢复操作)

        恢复是撤销的逆过程。从 mRecoverList 中取出命令,重新执行它,并将其移动到 mCancelList。其逻辑与撤销相对应。


文章转载自:

http://6opxkY1H.sgfpn.cn
http://13x3eGVb.sgfpn.cn
http://DBqK8eEY.sgfpn.cn
http://IaguBKs0.sgfpn.cn
http://fiUw4LXW.sgfpn.cn
http://BSmtZw32.sgfpn.cn
http://iHlihJu6.sgfpn.cn
http://38mJdPPt.sgfpn.cn
http://diZMnv61.sgfpn.cn
http://Tx1EyxnT.sgfpn.cn
http://0iI6UTbH.sgfpn.cn
http://E4ugvPjp.sgfpn.cn
http://DPZRIaOA.sgfpn.cn
http://8fAv5Fpk.sgfpn.cn
http://jutTjlPn.sgfpn.cn
http://WcxX0Z58.sgfpn.cn
http://f2dbHbSW.sgfpn.cn
http://962f21TJ.sgfpn.cn
http://sffVDC74.sgfpn.cn
http://b1qcHfwo.sgfpn.cn
http://IYcZJCY6.sgfpn.cn
http://qwTb4viN.sgfpn.cn
http://WcV6RoxZ.sgfpn.cn
http://fjp7OmvW.sgfpn.cn
http://vsFQd1wB.sgfpn.cn
http://4CNz6OYu.sgfpn.cn
http://LGYbPpQn.sgfpn.cn
http://MSXNbsah.sgfpn.cn
http://uM9K9lIP.sgfpn.cn
http://4qzpG5Uc.sgfpn.cn
http://www.dtcms.com/a/387469.html

相关文章:

  • 设计模式(C++)详解——组合模式(Composite Pattern)(2)
  • Android中获取用户的国家码
  • JVM性能优化总结
  • 【js】js将金额转千百十元角分的打印格式:
  • 硬件(十三)模拟转数字ADC转换
  • OpenEuler系统下部署MySQL数据库
  • 异步MySQL连接池实现
  • 用Python 连接 MySQL数据库测试实战脚本(文中含源代码)
  • vue中下载文件保存格式和加密方式
  • typescript和vue和node项目的构建打包部署
  • Chat2DB+cpolar组合突破物理限制,成为数据库查询新解
  • Power BI 组件 AI Chart 技术解析:自然语言驱动的可视化革新
  • 【Linux网络】网络传输基本流程
  • 【开题答辩全过程】以 Boss直聘网站数据分析与可视化为例,包含答辩的问题和答案
  • 基于 Node.js 的后端框架:NestJS 和 Express(一)
  • Python 2025:现代Web开发与数据分析的融合新趋势
  • 数据可视化:点亮数据背后的价值
  • 微信小程序答题考试源码系统+独立部署教程 适配学校 / 企业 / 培训机构
  • Apache JMeter介绍(开源的性能测试工具,主要用于对软件系统、服务器、网络或对象进行压力测试和性能测试)
  • 叠衣服的最优解:机器人如何用语言指令完成复杂家务
  • jmeter 数据库连接配置 JDBC Connection Configuration
  • 神经网络与深度学习基础:从线性回归到分类模型
  • Jmeter 参数、设置相关
  • jmeter 提取变量设置为全局变量
  • open61499:重新定义工业编程,让复杂自动化变简单
  • 基于MATLAB的支持向量数据描述算法
  • 超越重命名:如何利用高级规则实现文件的精准自动化分类保存
  • Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
  • C#开发常用方法汇总(类型转换)
  • 从踩坑到高效选型:基于 AI Ping 平台的 20+MaaS 供应商、220 + 模型服务性能(延迟 / 吞吐 / 可靠性):深度评测与大模型选型指南