天天看点

EditText实现undo/redo功能

EditText实现undo/redo功能

    • 一、设计思路
    • 二、已有的实现方案
      • 1. AndroidEdit
      • 2. MarkNote
      • 3. EditText
    • 三、通过反射调用undo/redo功能
      • 1. 需要实现的方法
      • 2. 最终代码
      • 3. 兼容Android 9.0
    • 四、Finally

一、设计思路

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.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】

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