Android实现undo/redo功能

Android实现undo/redo功能

一、目标

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

二、体验地址

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

三、功能设计

功能设计要求:

  1. 实现undo/redo功能
  2. 显示undo/redo状态,操作无法执行时,必须显示为不可用状态
  3. 支持从外部键盘通过快捷键执行undo/redo
  4. 外部键盘与操作按钮的操作行为必须同步
  5. 禁用长按弹出菜单的undo/redo功能(界面上已经提供按钮,不二做

四、准备工作

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

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

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

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

1
2
3
4
5
6
/**
* 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来完成这个功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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

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