EditText实现undo/redo功能

EditText实现undo/redo功能

一、设计思路

undo/redo有2种实现方式

  1. 数据——保存变化前的全部数据,undo时恢复之前的数据
  2. 变化——保存前后的数据变化,undo时执行相反的操作
实现方式 优点 缺点
数据 实现简单 占用更多的空间
变化 节省空间 实现复杂

采用方式1(数据),在文章字数多的时候,将会占用非常大的储存空间。

比如《红楼梦》第一回《甄士隐梦幻识识通灵 贾雨村风尘怀闺秀》全文7911字,接近8000字。

即使增加一个换行符,也会保存8000个字符。

随着修改量增加,内存急剧地消耗。

没有特殊的理由,尽量不使用保存数据的方式来实现。

因此,我们主要考虑第2种方式——保存变化的方式。

二、已有的实现方案

1. AndroidEdit

GitHub开源项目——Android EditText的撤销和恢复(反撤销)

项目地址:https://github.com/qinci/AndroidEdit

2. MarkNote

之前推荐过的一款Markdown编辑器,支持undo/redo功能。

项目地址:https://github.com/Shouheng88/MarkNote

3. EditText

不用怀疑,就是Android开发经常使用到的EditText。

在Android 6.0(API 23)之前,EditText已经支持undo/redo,只是没有开放API,必须通过反射的方式才能调用。

在Android 6.0(API 23)及以后的版本,undo/redo功能进一步升级。

可以调用onTextContextMenuItem接口,传入android.R.id.undoandroid.R.id.redo实现undo/redo。

如果连接外部键盘,还可以使用快捷键Ctrl + Z/Ctrl + Shift + Z完成undo/redo。

比较3种已有的实现方案,自然是选择Android系统自带的实现方案。

虽然在Android 6.0之前没有开放API,系统本身也没有调用这些API。

但Android 6.0及之后,已经可以通过外接键盘实现undo/redo,说明API已经不仅仅是测试功能,而是可以在生产环境中使用的功能。

因此,果断选择使用系统自身API。

三、通过反射调用undo/redo功能

1. 需要实现的方法

方法 功能 调用方式
undo() 执行undo 直接调用TextView#onTextContextMenuItem(android.R.id.undo)
canUndo() 判断能否undo,用于界面显示 反射调用TextView#canUndo()
redo() 执行redo 直接调用TextView#onTextContextMenuItem(android.R.id.redo)
canRedo() 判断能否redo,用于界面显示 反射调用TextView#canRedo()
forgetUndoRedo() 清空undo/redo 反射调用TextViw#mEditor,再次反射调用Editor#forgetUndoRedo()

2. 最终代码

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
public class TextViewUtils {

public static final boolean canUndo(TextView view) {

try {
Method method = TextView.class.getDeclaredMethod("canUndo");
method.setAccessible(true);
boolean result = (Boolean)method.invoke(view);

return result;
} catch (Exception e) {
e.printStackTrace();
}

return false;
}

public static final void undo(TextView view) {
int id = android.R.id.undo;

view.onTextContextMenuItem(id);
}

public static final boolean canRedo(TextView view) {

try {
Method method = TextView.class.getDeclaredMethod("canRedo");
method.setAccessible(true);
boolean result = (Boolean)method.invoke(view);

return result;
} catch (Exception e) {
e.printStackTrace();
}

return false;
}

public static final void redo(TextView view) {
int id = android.R.id.redo;

view.onTextContextMenuItem(id);
}

public static final void forgetUndoRedo(TextView view) {

try {

Field fEditor = TextView.class.getDeclaredField("mEditor");
fEditor.setAccessible(true);
Object editor = fEditor.get(view);

Class<?> clazz = editor.getClass();
Method method = clazz.getDeclaredMethod("forgetUndoRedo");
method.setAccessible(true);

method.invoke(editor);
} catch (Exception e) {
e.printStackTrace();
}
}
}

3. 兼容Android 9.0

从Android 9.0开始,谷歌开始限制开发者通过反射的方式调用未公开的API。

具体的限制,推荐阅读《android9.0后对hide方法反射限制的分析》。

在Android 9.0模拟器的测试结果如下:

  • Target SDK Version = 28
接口 Logcat信息
canUndo() Accessing hidden method Landroid/widget/TextView;->canUndo()Z (dark greylist, reflection)

java.lang.NoSuchMethodException: canUndo []
canRedo() Accessing hidden method Landroid/widget/TextView;->canRedo()Z (dark greylist, reflection)

java.lang.NoSuchMethodException: canRedo []
forgetUndoRedo() Accessing hidden field Landroid/widget/TextView;->mEditor:Landroid/widget/Editor; (light greylist, reflection)
Accessing hidden method Landroid/widget/Editor;->forgetUndoRedo()V (dark greylist, reflection)

java.lang.NoSuchMethodException: forgetUndoRedo []
  • Target SDK Version < 28
接口 Logcat信息
canUndo() Accessing hidden method Landroid/widget/TextView;->canUndo()Z (dark greylist, reflection)
canRedo() Accessing hidden method Landroid/widget/TextView;->canRedo()Z (dark greylist, reflection)
forgetUndoRedo() Accessing hidden field Landroid/widget/TextView;->mEditor:Landroid/widget/Editor; (light greylist, reflection)
Accessing hidden method Landroid/widget/Editor;->forgetUndoRedo()V (dark greylist, reflection)

对比2个结果。3个方法都处于dark greylist列表中。

当Target SDK Version < 28时,虽然给出警告信息,但调用是成功的。

当Target SDK Version = 28时,抛出NoSuchMethodException异常,调用失败。

因此,为保证能通过反射方式实现undo/redo方法。

千万,千万,千万不要将Target SDK Version设置等于或超过28,否则一定会调用失败。

千万,千万,千万不要将Target SDK Version设置等于或超过28,否则一定会调用失败。

千万,千万,千万不要将Target SDK Version设置等于或超过28,否则一定会调用失败。

四、Finally

如果发布的版本,已经将Target SDK Version设置为28。

千万,千万,千万不要降低Target SDK Version的版本,否则会导致升级安装失败。

千万,千万,千万不要降低Target SDK Version的版本,否则会导致升级安装失败。

千万,千万,千万不要降低Target SDK Version的版本,否则会导致升级安装失败。

最后,附上神马笔记最新版本下载地址:

神马笔记 版本1.4.0.apk

~忽闻海上有仙山~山在虚无缥缈间~