Android实现undo/redo功能

时间:2024-04-08 15:19:51

一、目标

实现编辑器的undo/redo功能。
Android实现undo/redo功能

二、体验地址

神马笔记最新版本:【神马笔记 版本1.4.0.apk

三、功能设计

功能设计要求:

  1. 实现undo/redo功能
  2. 显示undo/redo状态,操作无法执行时,必须显示为不可用状态
  3. 支持从外部键盘通过快捷键执行undo/redo
  4. 外部键盘与操作按钮的操作行为必须同步

四、准备工作

在上一篇文章中,已经介绍了Android的EditText控件如何实现undo/redo功能。

具体内容详见《EditText实现undo/redo功能》。

需要注意的是,图文混排的实现方式采用的是RecyclerView的方式,当插入图片时,其实是创建了多个EditText控件,而不是单个EditText控件。所以,无法通过undo功能撤销插入图片的操作。仅仅局限于EditText的文本操作。

与此同时,正如Editor中的一段注释所描述的,无法撤销Span操作,目前只能处理文本内容的变化。

/**
 * An InputFilter that monitors text input to maintain undo history. It does not modify the
 * text being typed (and hence always returns null from the filter() method).
 *
 * TODO: Make this span aware.
 */

五、组合起来

1. UndoEditor

TextViewUtils的功能再次进行封装。

2. ParagraphEdit

EditText再次封装,使之直接支持undo/redo。

3. UndoHelper

功能设计2要求——显示undo/redo状态,操作无法执行时,必须显示为不可用状态。

同时一篇文章可能有1个或多个EditText组成。

因此,在EditText切换焦点时,必须更新undo/redo按钮状态,以指示操作是否可以执行。

OnGlobalFocusChangeListener可以监听焦点控件的变化,从而实现这个功能。

另外,当EditText文字内容发生变化时,同样需要更新按钮状态。

我们使用TextWatcher来完成这个功能。

public class UndoHelper implements LifecycleObserver,
        ViewTreeObserver.OnGlobalFocusChangeListener,
        ViewTreeObserver.OnGlobalLayoutListener,
        TextWatcher {

    View decorView;

    View undoBtn;
    View redoBtn;

    Fragment parent;

    public UndoHelper(Fragment f, View undoBtn, View redoBtn) {
        this.parent = f;
        f.getLifecycle().addObserver(this);

        this.undoBtn = undoBtn;
        undoBtn.setEnabled(false);
        undoBtn.setOnClickListener(this::onUndoClick);

        this.redoBtn = redoBtn;
        redoBtn.setEnabled(false);
        redoBtn.setOnClickListener(this::onRedoClick);

        this.decorView = f.getActivity().getWindow().getDecorView();
        decorView.getViewTreeObserver().addOnGlobalFocusChangeListener(this);
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void onDestroy() {
        decorView.getViewTreeObserver().removeOnGlobalFocusChangeListener(this);
    }

    @Override
    public void onGlobalFocusChanged(View oldFocus, View newFocus) {
        this.updateButtons(newFocus);

        if (oldFocus != null && oldFocus instanceof ParagraphEdit) {
            ParagraphEdit focus = (ParagraphEdit)oldFocus;
            focus.removeTextChangedListener(this);
        }

        if (newFocus != null && newFocus instanceof ParagraphEdit) {
            ParagraphEdit focus = (ParagraphEdit)newFocus;
            focus.removeTextChangedListener(this);
            focus.addTextChangedListener(this);
        }
    }

    @Override
    public void onGlobalLayout() {
        this.updateButtons();
    }

    public void updateButtons() {
        View focus = parent.getActivity().getCurrentFocus();
        this.updateButtons(focus);
    }

    void onUndoClick(View view) {
        View focus = parent.getActivity().getCurrentFocus();
        this.undo(focus);
    }

    void onRedoClick(View view) {
        View focus = parent.getActivity().getCurrentFocus();
        this.redo(focus);
    }

    void updateButtons(View view) {

        boolean canUndo = false;
        boolean canRedo = false;

        if (view != null && (view instanceof ParagraphEdit)) {
            ParagraphEdit edit = (ParagraphEdit)view;
            UndoEditor e = edit.getUndoEditor();
            canUndo = e.canUndo();
            canRedo = e.canRedo();
        }

        undoBtn.setEnabled(canUndo);
        redoBtn.setEnabled(canRedo);
    }

    void undo(View view) {
        if (view != null && (view instanceof ParagraphEdit)) {
            ParagraphEdit edit = (ParagraphEdit)view;
            UndoEditor e = edit.getUndoEditor();
            if (e.canUndo()) {
                e.undo();
            }
        }

        this.updateButtons(view);
    }

    void redo(View view) {
        if (view != null && (view instanceof ParagraphEdit)) {
            ParagraphEdit edit = (ParagraphEdit)view;
            UndoEditor e = edit.getUndoEditor();
            if (e.canRedo()) {
                e.redo();
            }

        }

        this.updateButtons(view);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    @Override
    public void afterTextChanged(Editable s) {
        updateButtons();
    }
}

六、Finally

~为君持酒劝斜阳~且向花间留晚照~