【问题标题】:android best way to implement undo/redo on ArrayListandroid在ArrayList上实现撤消/重做的最佳方法
【发布时间】:2013-01-22 11:16:21
【问题描述】:

很抱歉这个问题的一般性质,但我不想从任何假设开始,因为害怕错过大局

我有一个文档编辑类型的应用程序(音乐符号),并且想要实现撤消和重做。 所有相关数据都保存在此

static  ArrayList <TTEvt> mEvList;

在我的 windows/MFC 应用程序中,我只是序列化数据结构并将其放在堆栈上。它使用大量内存,但简单且万无一失。

所以我想知道在 android 中保存和恢复我的 ArrayList 的最糟糕的方法是什么?

谢谢

【问题讨论】:

  • 列表有多大?相当短(一百)还是相当长(百万)? TTEvt 有多大?您是否控制访问该对象的所有代码,还是需要将其保留为 TTEvt?
  • 我不确定我是否理解那个帖子,尤其是 Runnable 的东西。 TTEvt 大约是 10 个整数,我控制它。对于一首 3 分钟的歌曲,我的列表通常包含 5000 个元素。我不认为保存每个添加和删除都会起作用,太复杂了,最好保存整个东西
  • 我想基本上我正在寻找一种方法来序列化并可能将 ArrayList 压缩到内存。但就像我说的,我不想假设这是最好的方法,以防万一有更好的东西。

标签: android serialization undo-redo


【解决方案1】:

这里是有效的多级撤消/重做代码。它工作正常,但它很慢。我取出了 gzip,这有点帮助,但它基本上使我的 UI 无法使用。但至少它有效,我可以尝试从这里优化。

    static LinkedList<byte[]> undoStack = new LinkedList<byte[]>();
    static LinkedList<byte[]> redoStack = new LinkedList<byte[]>();

    public static void addUndoCheckpoint() {

        long start = System.currentTimeMillis();

        byte[] byteBuf = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
            ObjectOutputStream objectOut = new ObjectOutputStream(baos);
            for (TTEvt ev : Doc.mEvList)
                objectOut.writeObject(ev);
            objectOut.close();
            byteBuf = baos.toByteArray();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        undoStack.push(byteBuf);

        if (undoStack.size() > 10)
            undoStack.removeLast(); // limit size

        redoStack.clear();

        long end = System.currentTimeMillis();
        Log.d("MainView", "addUndoCheckpoint time=" + (end - start) + "mS");
    }

    public static void doUndo() {

        if (undoStack.size() == 0)
            return;

        // push current state onto redo stack first
        byte[] byteBuf = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
            ObjectOutputStream objectOut = new ObjectOutputStream(baos);
            for (TTEvt ev : Doc.mEvList)
                objectOut.writeObject(ev);
            objectOut.close();
            byteBuf = baos.toByteArray();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        redoStack.push(byteBuf); // push current state onto redo stack
        if (redoStack.size() > 10)
            redoStack.removeLast();

        // now undo
        mEvList.clear();
        byteBuf = undoStack.pop();

        ObjectInputStream objectIn = null;
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(byteBuf);
            // GZIPInputStream gzipIn;
            // gzipIn = new GZIPInputStream(bais);
            objectIn = new ObjectInputStream(bais);
            while (true) {
                TTEvt ev = (TTEvt) objectIn.readObject();
                if (ev == null)
                    break;
                Doc.mEvList.add(ev);
            }
            objectIn.close();
        } catch (IOException e) {
            // this is the normal exit
            if (objectIn != null) {
                try {
                    objectIn.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }
            }
            // e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        MainView.forceTotalRedraw();
    }

    public static void doRedo() {

        if (redoStack.size() == 0)
            return;

        // push current state onto undo stack first so we can undo the redo
        byte[] byteBuf = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
            ObjectOutputStream objectOut = new ObjectOutputStream(baos);
            for (TTEvt ev : Doc.mEvList)
                objectOut.writeObject(ev);
            objectOut.close();
            byteBuf = baos.toByteArray();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        undoStack.push(byteBuf); // push current state onto redo stack
        if (undoStack.size() > 10)
            undoStack.removeLast();

        // now redo
        mEvList.clear();
        byteBuf = redoStack.pop();

        ObjectInputStream objectIn = null;
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(byteBuf);
            // GZIPInputStream gzipIn;
            // gzipIn = new GZIPInputStream(bais);
            objectIn = new ObjectInputStream(bais);
            while (true) {
                TTEvt ev = (TTEvt) objectIn.readObject();
                if (ev == null)
                    break;
                Doc.mEvList.add(ev);
            }
            objectIn.close();
        } catch (IOException e) {
            // this is the normal exit
            if (objectIn != null) {
                try {
                    objectIn.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }
            }
            // e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        MainView.forceTotalRedraw();

    }
}

请注意,您应该在更改 ArrayList 之前调用 addUndoCheckpoint。我目前正在开发一个您可以在更改 ArrayList 后调用的版本。这样做的好处是您可以在后台执行 addUndoCheckpoint 而不会减慢 UI。

【讨论】:

  • 我确实进行了速度优化,但它不是一般的东西,所以我不需要在这里展示它。基本上,我的数据被划分为逻辑单元,在我的情况下是音乐条。我可以只保存我在 addUndoCheckpoint 中更改的唯一条,这很快。所以基本上我把数据分成更小的单位,只保存/撤消那些。
【解决方案2】:

仅供参考,我想出了几个选项。

最简单的方法是使用 toArray 将 ArrayList 写入一个简单的对象数组,如下所示:

static TTEvt evArray[];

    public static void addUndoCheckpoint() {

        long start  = System.currentTimeMillis();

        evArray = Doc.mEvList.toArray(new TTEvt[Doc.mEvList.size()]);

        long end = System.currentTimeMillis();
        Log.d("MainView", "addUndoCheckpoint time="+(end-start)+"mS");
    }
    public static void doUndo() {

        Doc.mEvList.clear();
        for(TTEvt ev : evArray)
            Doc.mEvList.add(ev);

        forceTotalRedraw();
    }

更新:我刚刚发现上面的代码并没有真正起作用,因为 toArray 只对对象的引用数组执行 new,而不是对象本身。所以我还需要克隆所有对象,这显然是很多内存,可能还有时间。也许答案是下面的慢选项,使用序列化但在同步线程中执行,这样它就不会减慢 UI。

使用 gzip 压缩来节省空间的更复杂的方法是这样的

  static byte[] undoBuf;

  public static void addUndoCheckpoint() {

    long start  = System.currentTimeMillis();

    //evArray = Doc.mEvList.toArray(new TTEvt[Doc.mEvList.size()]);

    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
        ObjectOutputStream objectOut = new ObjectOutputStream(gzipOut);
        for(TTEvt ev: Doc.mEvList)
            objectOut.writeObject(ev);
        objectOut.close();
        undoBuf = baos.toByteArray();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    long end = System.currentTimeMillis();
    Log.d("MainView", "addUndoCheckpoint time="+(end-start)+"mS");
  }

  public static void doUndo() {

    Doc.mEvList.clear();
    //for(TTEvt ev : evArray)
    //  Doc.mEvList.add(ev);

    try {
        ByteArrayInputStream bais = new ByteArrayInputStream(undoBuf);
        GZIPInputStream gzipIn;
        gzipIn = new GZIPInputStream(bais);
        ObjectInputStream objectIn = new ObjectInputStream(gzipIn);
        while(objectIn.available()>0) {
            TTEvt ev = (TTEvt) objectIn.readObject();
            Doc.mEvList.add(ev);
        }
        objectIn.close();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }


    forceTotalRedraw();
}

问题在于它的速度很慢,对于大约 5000 个条目的数据结构来说几乎是 1 秒

目前这只是一个 1 级撤消,目前实现了一个堆栈以保持多个级别,并在内存中寻找更快的压缩方式

【讨论】:

    【解决方案3】:

    您可以使用 Memento 设计模式:这里有一个使用此模式的示例撤消/重做 http://www.youtube.com/watch?v=jOnxYT8Iaoo&list=PLF206E906175C7E07&index=25

    【讨论】:

      【解决方案4】:

      解决此问题的一种方法不是从数组中删除项目,而只是将它们标记为已删除并刷新视图以使它们消失。所以重做只会取消标记它们。根据应用程序的逻辑,在某些操作中,应从数组中永久删除已删除的项目。

      还可以有一个单独的数组来存放撤消项,因为数组只保留对对象的引用,因此它不会占用太多内存并且可以限制大小。

      但如果撤消/重做需要在设备关闭后继续存在,那么将每个项目的值保存到文件或数据库是唯一的选择。

      【讨论】:

      • 可以在数组列表中添加、删除或更改对象。试图只跟踪更改会非常复杂。
      • 我想为了让它更快,我会创建两个额外的数组。一个会保留最后 20 个对象的克隆(例如),而另一个会记录操作类型,即添加、删除或更改以及应用程序可以在需要时对当前对象和克隆对象的引用执行的其他功能.所以有两个历史:动作和对象。
      猜你喜欢
      • 1970-01-01
      • 2017-12-07
      • 2017-03-16
      • 2020-08-11
      • 2016-02-18
      • 1970-01-01
      • 2011-03-10
      • 2011-11-14
      • 2010-09-17
      相关资源
      最近更新 更多