概要
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
をセットすると、アプリ上でアンドゥ・リドゥが使えるようになる。
参考