Android实现undo/redo功能
一、目标
实现编辑器的undo/redo功能。
二、体验地址
神马笔记最新版本:【神马笔记 版本1.4.0.apk】
三、功能设计
功能设计要求:
- 实现undo/redo功能
- 显示undo/redo状态,操作无法执行时,必须显示为不可用状态
- 支持从外部键盘通过快捷键执行undo/redo
- 外部键盘与操作按钮的操作行为必须同步
四、准备工作
在上一篇文章中,已经介绍了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
~为君持酒劝斜阳~且向花间留晚照~