Android:基於EditText實現撤銷和重做機制
阿新 • • 發佈:2019-02-13
一、 場景描述和思路分析
說到撤銷和重做想必大家腦海中浮現的一定是Ctrl+Z、Ctrl+Y這兩個快捷鍵,平常生產開發的時候也少不了要和這兩個按鍵打交道。作為一個開發者筆者自然對其中的實現方法感到好奇,想必閱讀此文的你也是一樣的。
如果你稍微懂點資料結構並且有著基礎的封裝思想的話,大體都能想到一些思路:
將使用者操作抽象成一個介面,介面包含undo()和redo()兩個方法,並用棧來記錄操作的順序,通過出入棧和呼叫兩個方法來處理撤銷和重做的邏輯。
涉及到撤銷和重做的大部分都是需要使用者編輯的功能,如果你想在Android上基於EditText開發出一個文字編輯器的話,那麼按照這個思路一步步實現肯定是沒有問題的。
我們發現文字的編輯操作其實可以簡化為插入、刪除。使用者選中文字後貼上的操作,也就是替換,可以分解為刪除選中文字後插入貼上板內容。
接下來只要記錄下輸入和刪除的操作就可以儲存使用者的操作了,這裡我們可以使用EditText提供了TextWatcher
用於監聽文字變化。
接下來請看程式碼實現。
二、 程式碼實現
首先我們需要實現編輯操作類,程式碼如下:
class EditOperation implements Parcelable, Serializable {
//原始內容,通常是被刪除的部分
private String src;
private int srcStart;
private int srcEnd;
//目標內容,通常是輸入的部分
private String dst;
private int dstStart;
private int dstEnd;
EditOperation setSrc(CharSequence src, int srcStart, int srcEnd) {
this.src = src != null ? src.toString() : "";
this.srcStart = srcStart;
this .srcEnd = srcEnd;
return this;
}
EditOperation setDst(CharSequence dst, int dstStart, int dstEnd) {
this.dst = dst != null ? dst.toString() : "";
this.dstStart = dstStart;
this.dstEnd = dstEnd;
return this;
}
void undo(EditText text) {
Editable editable = text.getText();
int idx = -1;
if (dstEnd > 0) {//刪除目標內容
editable.delete(dstStart, dstEnd);
if (src == null) {
idx = dstStart;
}
}
if (src != null) {//插入原始內容
editable.insert(srcStart, src);
idx = srcStart + src.length();
}
if (idx >= 0) {//恢復游標位置
text.setSelection(idx);
}
}
void redo(EditText text) {
Editable editable = text.getText();
int idx = -1;
if (srcEnd > 0) {//刪除原始內容
editable.delete(srcStart, srcEnd);
if (dst == null) {
idx = srcStart;
}
}
if (dst != null) {//插入目標內容
editable.insert(dstStart, dst);
idx = dstStart + dst.length();
}
if (idx >= 0) {//恢復游標位置
text.setSelection(idx);
}
}
}
之後我們要在使用者編輯文字的時候生成對應的EditOperation
例項,也就是實現TextWatcher
:
public class OperationManager implements TextWatcher {
private EditOperation opt;
//啟用開關,用於過濾撤銷/重做時的編輯操作
private boolean enable = true;
OperationManager disable() {
enable = false;
return this;
}
OperationManager enable() {
enable = true;
return this;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (count > 0) {
int end = start + count;
if (enable) {
if (opt == null) {
opt = new EditOperation();
}
//記錄原始內容
opt.setSrc(s.subSequence(start, end), start, end);
}
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (count > 0) {
int end = start + count;
if (enable) {
if (opt == null) {
opt = new EditOperation();
}
//記錄目標內容
opt.setDst(s.subSequence(start, end), start, end);
}
}
}
@Override
public void afterTextChanged(Editable s) {
if (enable && opt != null) {
if (!redoOpts.isEmpty()) {//重做棧不空時使用者又編輯了文字,視為拋棄重做棧
redoOpts.clear();
}
//將操作入棧
undoOpts.push(opt);
}
opt = null;
}
//使用LinkedList代替棧
private final LinkedList<EditOperation> undoOpts = new LinkedList<>();
private final LinkedList<EditOperation> redoOpts = new LinkedList<>();
}
之後的撤銷重做就很簡單了:
public boolean undo() {
if (canUndo()) {
EditOperation undoOpt = undoOpts.pop();
//遮蔽撤銷產生的事件
disable();
undoOpt.undo(editText);
enable();
//填入重做棧
redoOpts.push(undoOpt);
return true;
}
return false;
}
public boolean redo() {
if (canRedo()) {
EditOperation redoOpt = redoOpts.pop();
//遮蔽重做產生的事件
disable();
redoOpt.redo(editText);
enable();
//填入撤銷
undoOpts.push(redoOpt);
return true;
}
return false;
}
最後實現效果如圖: