概要
AndroidアプリのEditTextで、アンドゥ(元に戻す)・リドゥ(やり直し)ができるようにしたかった。しかし、標準ライブラリではアンドゥ・リドゥ機能は用意されていないらしい(多分)。
アンドゥ・リドゥを実装する方法を調べた。
アンドゥ・リドゥを実現する方法
android.textパッケージにTextWatcherというインターフェースが用意されているため、それを利用して実装する。
TextWatcherとは
When an object of this type is attached to an Editable, its methods will be called when the text is changed.
Editableにこの型のオブジェクトが付いているとき、テキストが変更されたときにTextWatcherのメソッドが呼ばれる。
呼ばれるメソッドは以下の3つである。TextWatcherはインターフェースであるため、3つともオーバーライドする必要がある。
-
beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int)
s中のstart番目から始まるcount個の文字が、長さafterの新しいテキストに置き換えられようとしているときに呼ばれるメソッド -
onTextChanged(s: CharSequence, start: Int, before: Int, count: Int)
s中のstart番目から始まるcount個の文字が、長さbeforeの古いテキストを置き換えたときに呼ばれるメソッド -
afterTextChanged(s: Editable)
s中のどこかで、テキストが変更されたときに呼ばれるメソッド
class UndoTextWatcher : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// テキスト変更が起こる直前の処理(undo用の文字列保持など)
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など)
}
override fun afterTextChanged(s: Editable) {
// テキストが変更された後の処理(undo・redoの可否判定など)
}
}
例えば「あいうえお」→「あいうえおかきくけこ」の場合は以下のようになる。
System.out: beforeTextChanged // (「お」の直後の)0文字が5文字のafterに置き換えられようとしている
System.out: s: あいうえお, start: 5, count: 0, after: 5
System.out: onTextChanged // 「か」から5文字が0文字のbeforeを置き換えた
System.out: s: あいうえおかきくけこ, start: 5, before: 0, count: 5
System.out: afterTextChanged // 「あいうえおかきくけこ」になった
System.out: s: あいうえおかきくけこ
挙動を実装する
まず、undoing(アンドゥ・リドゥ実行中かどうか)・undos(アンドゥのリスト)・redos(リドゥのリスト)と、beforeやafterを保持するクラスを用意しておく。
class EditEvent {
private val beforePosition: Int // 置き換えられる文字列の開始位置
private val before: CharSequence // 置き換えられる文字列
private var afterPosition: Int = 0 // 置き換えた文字列の開始位置
private var after: CharSequence = "" // 置き換えた文字列
constructor(beforePosition: Int, before: CharSequence) {
this.beforePosition = beforePosition
this.before = before
}
fun setAfter(afterPosition: Int, after: CharSequence) {
this.afterPosition = afterPosition
this.after = after
}
fun undo(editable: Editable) {
editable.replace(afterPosition, afterPosition + after.length, before)
}
fun redo(editable: Editable) {
editable.replace(beforePosition, beforePosition + before.length, after)
}
}
class UndoTextWatcher : TextWatcher {
var undoing = false // undo・redo実行中かどうか
val undos = LinkedList<EditEvent>() // undoリスト
val redos = LinkedList<EditEvent>() // redoリスト
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// テキスト変更が起こる直前の処理(undo用の文字列保持など)
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など)
}
override fun afterTextChanged(s: Editable) {
// テキストが変更された後の処理(undo・redoの可否判定など)
}
}
beforeTextChangedでやること
[0] (undoingがtrueのとき、何もしない。)
[1] startと、元文字列sのstartからcount文字(==置き換えられる文字列)をundosに追加する。
[2] redosが空でない場合、redosを空にする。
class UndoTextWatcher : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// テキスト変更が起こる直前の処理(undo用の文字列保持など)
if (undoing) return // [0]
val event = EditEvent(start, s.subSequence(start, start + count)) // undoする文字列と位置を保持する
undos.addLast(event) // [1]
clearRedos() // [2]
}
private fun clearRedos() {
while (!redos.isEmpty()) {
redos.removeFirst()
}
}
...(略)...
}
onTextChangedでやること
[0] (undoingがtrueのとき、何もしない。)
[1] beforeTextChangedでundosに追加した要素に、startと、新文字列sのstartからcount文字(==置き換えた文字列)の情報を追加する。
class UndoTextWatcher : TextWatcher {
...(略)...
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など)
if (undoing) return // [0]
val event = undos.getLast() // undoリストに最後に追加した要素
event.setAfter(start, s.subSequence(start, start + count)) // [1]
}
...(略)...
}
アンドゥボタンが押されたときにやること
[0] (undosが空の場合何もしない。)
[1] undosから最新の要素を取り出して、redosに追加する。
[2] undoingをtrueにする。
[3] 文字列全体のafterPositionからafterの長さ分の文字列(つまりafter)を、beforeに置き換える。
[4] undoingをfalseにする。
class UndoTextWatcher : TextWatcher {
...(略)...
fun undo(editable: Editable) {
if (undos.isEmpty()) return // [0]
val event = undos.removeLast() // [1]
redos.addLast(event) // [1]
undoing = true // [2]
try {
event.undo(editable) // [3]
} finally {
undoing = false // [4]
}
}
}
リドゥボタンが押されたときにやること
[0] (redosが空の場合何もしない。)
[1] redosから最新の要素を取り出して、undosに追加する。
[2] undoingをtrueにする。
[3] 文字列全体のbeforePositionからbeforeの長さ分の文字列(つまりbefore)を、afterに置き換える。
[4] undoingをfalseにする。
class UndoTextWatcher : TextWatcher {
...(略)...
fun redo(editable: Editable) {
if (redos.isEmpty()) return // [0]
val event = redos.removeLast() // [1]
undos.addLast(event) // [1]
undoing = true // [2]
try {
event.redo(editable) // [3]
} finally {
undoing = false // [4]
}
}
}
これでアンドゥ・リドゥ機能の挙動の実装は完成した。
EditTextで使えるようにする
実際にアプリの画面でアンドゥ・リドゥできるようにする。
EditText・undoボタン・redoボタンは適当に用意しておく。
val textWatcher = UndoTextWatcher()
editText.addTextChangedListener(textWatcher) // undo・redo機能を付けたTextWatcherをEditTextにセットする
undoButton.setOnClickListener {
textWatcher.undo(editText.text) // undoボタンを押してundoする
}
redoButton.setOnClickListener {
textWatcher.redo(editText.text) // redoボタンを押してredoする
}
これで、アプリ上でアンドゥ・リドゥを実行できるようになる。
その他
アンドゥ・リドゥできるか判定する
これ以上アンドゥできない時にボタンを無効にしたいなどの場合に、アンドゥできるかどうかを判定する。
class UndoTextWatcher : TextWatcher {
...(略)...
override fun afterTextChanged(s: Editable) {
hasUndos = !undos.isEmpty() // undoできるか(undoリストが空でないか)
hasRedos = !redos.isEmpty() // redoできるか(redoリストが空でないか)
}
}
if (!hasUndos) {
// ボタン無効などの処理
}
if (!hasRedos) {
// ボタン無効などの処理
}
まとめ
-
TextWatcherを利用して置き換える文字列や位置を取得し、アンドゥ・リドゥ機能を実装した。 -
EditText.addTextChangedListener()でTextWatcherをセットすると、アプリ上でアンドゥ・リドゥが使えるようになる。
参考
