EditText实现undo/redo功能
-
- 一、设计思路
- 二、已有的实现方案
-
- 1. AndroidEdit
- 2. MarkNote
- 3. EditText
- 三、通过反射调用undo/redo功能
-
- 1. 需要实现的方法
- 2. 最终代码
- 3. 兼容Android 9.0
- 四、Finally
一、设计思路
undo/redo有2种实现方式
- 数据——保存变化前的全部数据,undo时恢复之前的数据
- 变化——保存前后的数据变化,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.undo
或
android.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. 最终代码
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】
~忽闻海上有仙山~山在虚无缥缈间~