需求
撤销与重做功能
思考
关于记录的数据的两点思考:
- 记录操作
- 记录影响显示和逻辑的所有数据
很显然这里就要考虑取舍了:
记录操作 这种方案只需要记录每一步的操作,具体这个操作要怎么渲染和实现出来完全需要自己去实现,这种方式的好处是记录的数据量比较小,节省内存,但业务逻辑复杂,适合复杂庞大的数据记录
记录影响显示的所有数据 这种方案要记录所有影响显示和逻辑的所有数据,无疑,这种数据量肯定是比第一种大的,内存占用会比较大,但是业务逻辑简单
这里由于我的数据量不大所以选择了第二种方式来进行处理
首先肯定要有一个Manager来负责管理数据并实现撤销和重做的分发(我命名为UndoManager),还要有一个数据对象来记录每一步的数据,由于需要记录的信息未知,可能是一个json也可能是一个对象,所以这里用最抽象的接口来表示每一步的数据信息,后续怎么处理这份数据完全根据项目需求自己定制(我命名为IRecordData)
Redo Undo的功能都是先进后出的数据结构,这种数据结构对应到程序世界的话刚好对应的是Stack,这里就先用Stack来进行思考
定义有两个Stack 分别是UndoStack 和 RedoStack 分别来存储待撤销记录和待重做记录,假设这两个Stack的当前状态如下:
假设这个时候来个一个新的步骤Step4 ,这个时候Step4需要记录,记录的过程如下:
假设我现在需要撤销一步回到Step3,具体过程如下:
假设现在我又想重做上一步,想回到Step4,具体过程如下:
至此我们的记录、撤销、重做的核心逻辑就已经基本梳理清楚了,但是发现Record还有一个小问题,问题如下:
当我们RedoStack中存在数据时,如果记录新的步骤Step4,成功之后我们会发现,现在Undo会回到Step1,Redo会回到Step2,这样流程就乱了,所以,修改一个Record的逻辑,修改后如下:
有两种处理方案,方案一为每次记录新数步骤时清空RedoStack,方案二为每次记录新数据时将RedoStack中的数据尽数移动到UndoStack中,可根据需求选择。
由于我们选择的数据记录是记录影响显示和逻辑的所有数据 本身内存占用就比记录操作要大,如果再对这两个Stack的容量不做限制,就可能会浪费很多的内存,导致程序内存占用过高。所以我们需要对UndoStack和RedoStack做容量限制,超过容量时最早进入Stack的数据直接丢弃,但Stack本身是先进后出的数据结构,显然不能支持这种操作,所以权衡利弊之后,我选择将Stack改为LinkedList,具体的操作流程是一样的。
实现
下面直接展示源码:
using System.Collections.Generic;
namespace S
{
public interface IRecordData
{
}
public class UndoManager
{
private LinkedList<IRecordData> undoLinkedList;
private LinkedList<IRecordData> redoLinkedList;
private IRecordData currentData;
/// <summary>
/// Undo数量
/// </summary>
public int undoCount => undoLinkedList == null ? 0 : undoLinkedList.Count;
/// <summary>
/// Redo数量
/// </summary>
public int redoCount => redoLinkedList == null ? 0 : redoLinkedList.Count;
/// <summary>
/// 记录的最大容量 <=0时 代表无限容量
/// </summary>
public int maxCount=0;
public UndoManager()
{
undoLinkedList = new LinkedList<IRecordData>();
redoLinkedList = new LinkedList<IRecordData>();
currentData = null;
}
/// <summary>
/// 重做一步
/// </summary>
/// <returns></returns>
public IRecordData Redo()
{
if (currentData != null)
{
undoLinkedList.AddLast(currentData);
}
if (redoLinkedList.Count == 0)
{
currentData = null;
}
else
{
currentData = redoLinkedList.Last.Value;
redoLinkedList.RemoveLast();
}
return currentData;
}
/// <summary>
/// 撤销一步
/// </summary>
/// <returns></returns>
public IRecordData Undo()
{
if (currentData != null)
{
redoLinkedList.AddLast(currentData);
}
if (undoLinkedList.Count == 0)
{
currentData = null;
}
else
{
currentData = undoLinkedList.Last.Value;
undoLinkedList.RemoveLast();
}
return currentData;
}
/// <summary>
/// 记录
/// </summary>
/// <param name="recordData">记录对象</param>
/// <param name="overrideRedo">是否覆盖Redo链表</param>
public void Record(IRecordData recordData,bool overrideRedo=true)
{
if (currentData != null)
{
undoLinkedList.AddLast(currentData);
}
if (overrideRedo)
{
redoLinkedList.Clear();
}
else
{
while (redoLinkedList.Last != null)
{
IRecordData value = redoLinkedList.Last.Value;
redoLinkedList.RemoveLast();
undoLinkedList.AddLast(value);
}
}
currentData = recordData;
if (maxCount>0&&undoCount > maxCount) //容量维护
{
undoLinkedList.RemoveFirst();
}
}
/// <summary>
/// 清空undo&redo
/// </summary>
public void Clear()
{
undoLinkedList.Clear();
redoLinkedList.Clear();
}
}
}