このエントリーをはてなブックマークに追加このエントリをつぶやく

Android/EditTextでIMEの未確定文字列が確定された瞬間

長いことご無沙汰でした。

今回は地図アプリの話題から離れて、最近のアプリ製作の中でハマったというか難問だった問題について説明したいと思います。

AndroidのEditTextで入力中、確定キーを押した瞬間に何か処理をしたい、というリクエストがありました。で、これに対応すべく調査開始したのですが・・・なにせIMEの未確定状態が確定した瞬間のイベントが存在しないようなのです。

確定状態ならば http://wada811.blog.fc2.com/blog-entry-22.html のようにOnEditorActionListenerを使うことで確定キーが押されたことはわかります。ですが未確定文字がある状態から確定されたことはこのListenerでは取れませんでした。

 

次に出てきたのは http://joityui.blogspot.jp/2011/06/androidedittext.html で、InputFilterをつかって未確定文字があることを判断することができるようだ、ということはわかりました。ところが、確定キーを押した瞬間はこのInputFilter#filterは呼ばれないんですね・・・

また、 https://groups.google.com/forum/?fromgroups=#!topic/android-group-japan/aAFS2jKLjas にはSpannedを未確定文字の判断として使うのは間違い、という情報もあり、この方法は諦めました。

次に目をつけたのはEditable#addTextChangeListenerです。ここに渡すTextWatcherインターフェースでは変更前(beforeTextChanged)・入力後(onTextChanged)・変更後(afterTextChanged)の状態に合わせてメソッドを呼び出してくれます。ここで、文字列が確定された時にその文字列が通知されれば・・・!と思ったのですが、実際には未確定文字も含めて渡されていました。

どうにもこうにも行き詰ってしまったのですが、https://groups.google.com/forum/#!topic/android-group-japan/YRyOmjGe3WE にIMEの未確定状態をクリアするのにこのメソッドが使えるのでは?というコメントがあったので EditTextのクラスリファレンスの http://developer.android.com/intl/ja/reference/android/widget/TextView.html#clearComposingText() を眺めていたら、clearComposingText()はspanをクリアするということが書いてありました。
EditText#getEditableText()で取得できるEditableオブジェクトはspannedインターフェースを実装していますので、Editableに含まれるspannedの情報を眺めれば何かわかるのではないか?と思って、以下のコードを書いてみました。
[cce_java]
EditText edit1 = (EditText) findViewById(R.id.edit1);

edit1.addTextChangedListener(new TextWatcher()
{
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

@Override
public void afterTextChanged(Editable s) {
Object[] spanned = s.getSpans(0, s.length(), Object.class);
if (spanned != null) {
for (Object obj : spanned) {
Log.v(“”, obj.getClass().toString());
}
}
}
});
[/cce_java]
結果は以下の通りでした(Android emulator/SDKVersion 8で実行)

確定文字だけの場合
10-25 09:34:11.671: V/(298): class android.text.DynamicLayout$ChangeWatcher
10-25 09:34:11.671: V/(298): class android.widget.TextView$ChangeWatcher
10-25 09:34:11.671: V/(298): class android.text.method.TextKeyListener
10-25 09:34:11.671: V/(298): class android.text.Selection$START
10-25 09:34:11.671: V/(298): class android.text.Selection$END
10-25 09:34:11.671: V/(298): class android.text.method.ArrowKeyMovementMethod$OnePointFiveTapState
10-25 09:34:11.671: V/(298): class android.text.method.ArrowKeyMovementMethod$DoubleTapState

未確定文字がある場合
10-25 09:34:10.751: V/(298): class android.text.DynamicLayout$ChangeWatcher
10-25 09:34:10.751: V/(298): class android.widget.TextView$ChangeWatcher
10-25 09:34:10.751: V/(298): class android.text.method.TextKeyListener
10-25 09:34:10.751: V/(298): class android.text.Selection$START
10-25 09:34:10.761: V/(298): class android.text.Selection$END
10-25 09:34:10.761: V/(298): class android.text.method.ArrowKeyMovementMethod$OnePointFiveTapState
10-25 09:34:10.761: V/(298): class android.text.method.ArrowKeyMovementMethod$DoubleTapState
10-25 09:34:10.761: V/(298): class android.text.style.UnderlineSpan
10-25 09:34:10.761: V/(298): class android.view.inputmethod.ComposingText

class android.view.inputmethod.ComposingText

これこそがまさに欲しい情報なのでは!ということで、以下のように修正しようとしたところ…
[cce_java]
@Override
public void afterTextChanged(Editable s) {
Object[] spanned = s.getSpans(0, s.length(), Object.class);
if (spanned != null) {
for (Object obj : spanned) {
if (obj instanceof android.view.inputmethod.ComposingText) {
}
}
}
}
[/cce_java]
android.view.inputmethod.ComposingTextは非公開クラスのようで、コンパイルできませんでした。無念・・・

もちろん、クラス名をとって文字列で比較する、というパターンにすることも可能だと思います。
ですが、今回はもうひとつの違いである
class android.text.style.UnderlineSpan
の方に注目しました。これは未確定文字列に下線が引かれていることと一致しますね。
ただ、下線が全てのIMEで未確定文字を表すかどうかまでは保証できません。
一応社内にあったIMEで以下のものは上記の判断で行けそうということが分かっています。

  • Google 日本語入力Beta
  • POBox Touch(日本語)
  • Samsung日本語キーボード
  • ATOK(日本語)
  • iWnn IME for Android

最終的なコードは以下のようになりました。
文字列長で判断しているのは、連文節変換で複数の文節が未確定の状態で分節一つを確定した時にTextWatcherインターフェースに渡される文字列が思った通りではなかったためです。

[cce_java]
edit1.addTextChangedListener(new TextWatcher()
{
int currentLength = 0;

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
currentLength = s.toString().length();
}

@Override
public void afterTextChanged(Editable s) {
Log.v(“”, “after:” + s.toString());
if (s.toString().length() < currentLength) {
return;
}
boolean unfixed = false;
Object[] spanned = s.getSpans(0, s.length(), Object.class);
if (spanned != null) {
for (Object obj : spanned) {
if (obj instanceof android.text.style.UnderlineSpan) {
unfixed = true;
}
}
}
if (!unfixed) {
Toast toast = Toast.makeText(getApplicationContext(), "確定", Toast.LENGTH_SHORT);
toast.show();
}
}
});
[/cce_java]

この判定方法は
・EditTextにアプリケーションが下線を自分で加えない
・IMEの未確定文字列が下線で表現されている
場合に有効だと思われます。

もちろん、将来的にIMEの実装が変わってこの前提条件が崩れたり、内部実装が変わってこの判断方法が使えなくなる可能性はあります。
ですが、未確定文字列が確定した瞬間が取れるというのは、なかなか使いでがあると思います。

藤田 の紹介

スピリテックのCTOで今ココなう!の中の人。
カテゴリー: プログラミング   タグ: , ,   この投稿のパーマリンク

コメントは受け付けていません。