2012年10月12日金曜日

Android TextView (EditText) の文字選択処理をカスタマイズする

EditText (もしくは TextView で android:textIsSelectable="true" を指定した場合)に文字列をロングタップして起動する ActionMode をカスタマイズすることができます。

TextView の setCustomSelectionActionModeCallback() で ActionMode.Callback を指定することで、既存のメニューを削除したり、新しいメニューを追加したりすることができます。

EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } @Override public void onDestroyActionMode(ActionMode mode) { // TODO Auto-generated method stub } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // TODO Auto-generated method stub return false; } });

setCustomSelectionActionModeCallback() で渡した ActionMode.Callback は TextView の mCustomSelectionActionModeCallback で保持されます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#10075
350 private Callback mCustomSelectionActionModeCallback; ... 10075 public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) { 10076 mCustomSelectionActionModeCallback = actionModeCallback; 10077 } 10078 10079 /** 10080 * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null. 10081 * 10082 * @return The current custom selection callback. 10083 */ 10084 public ActionMode.Callback getCustomSelectionActionModeCallback() { 10085 return mCustomSelectionActionModeCallback; 10086 }

1. ActionMode を起動しない

onCreateActionMode() で false を返すと、ロングタップしても ActionMode が起動しなくなります。

EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { ... @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { return false; } }); TextView の ActionMode である SelectionActionModeCallback の onCreateActionMode() の中で mCustomSelectionActionModeCallback の onCreateActionMode() を呼び出し、その戻り値が false の場合は false を返すようになっているからです。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#10238
10182 private class SelectionActionModeCallback implements ActionMode.Callback { 10183 10184 @Override 10185 public boolean onCreateActionMode(ActionMode mode, Menu menu) { ... 10237 10238 if (mCustomSelectionActionModeCallback != null) { 10239 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) { 10240 // The custom mode can choose to cancel the action mode 10241 return false; 10242 } 10243 } ... 10251 }

2. 既存のメニュー項目を削除する

デフォルトのメニュー項目のそれぞれの ID は

SelectAll : android.R.id.selectAll
Cut : android.R.id.cut
Copy : android.R.id.copy;
Paste : android.R.id.paste

です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#9042 9041 // Selection context mode 9042 private static final int ID_SELECT_ALL = android.R.id.selectAll; 9043 private static final int ID_CUT = android.R.id.cut; 9044 private static final int ID_COPY = android.R.id.copy; 9045 private static final int ID_PASTE = android.R.id.paste; 例えば、Cut と Paste 機能を削除したい場合は removeItem() を使います。 EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { ... @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { menu.removeItem(android.R.id.cut); menu.removeItem(android.R.id.paste); return true; } });

3. メニューの機能を置き換える

メニューの項目はそのままで、タップされたときの処理を置き換えるには onActionItemClicked() で true を返します。もともとの処理も行ってほしい場合は false を返します。

例えば、MenuItem の id が android.R.id.selectAll のときに true を返すようにすると、全選択をタップしても何も起こらなくなります。

EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { ... @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { int id = item.getItemId(); switch(id) { case android.R.id.selectAll: // 独自の処理 return true; } return false; } });

4. 独自のメニュー項目を追加する

メニュー項目を追加するには onCreateActionMode で Menu.add() を使います。
残念ながら既存のメニュー項目の Order が 0 になっているため、任意の位置に追加することはできないようで、最後の位置に追加されます。さらに、Overflow menu に入ると、展開したときに EditText からフォーカスが外れて ActionMode が終了するという残念なことになります。

もう一つ残念なのが、メニュー項目をタップされたときに ActionMode を終了するための stopSelectionActionMode() というメソッドが private なため外部から呼べません(せめて protected にしてほしい)。
ただし、setText() し直すと選択が解除されるので ActionMode を終了することができます。


選択した文字が全角カナだったら半角カナにして先頭に "シャバドゥビタッチ" *1 をつけるようにしてみました。
(アイコンはがんばってトレースしました。)

R.id.replace は XML で定義しました。追加するメニューの ID は適当な数字ではなく、XML で定義しておくのがいいと思います。More Resource Type - ID

public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return true; } @Override public void onDestroyActionMode(ActionMode mode) { } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { menu.removeItem(android.R.id.paste); menu.removeItem(android.R.id.cut); menu.removeItem(android.R.id.copy); MenuItem item = menu.add(Menu.NONE, R.id.replace, Menu.NONE, "Replace"); item.setIcon(R.drawable.ic_replace); return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { CharSequence text = editText.getText(); int min = 0; int max = text.length(); if (editText.isFocused()) { final int selStart = editText.getSelectionStart(); final int selEnd = editText.getSelectionEnd(); min = Math.max(0, Math.min(selStart, selEnd)); max = Math.max(0, Math.max(selStart, selEnd)); } int id = item.getItemId(); switch (id) { case R.id.replace: CharSequence sub = text.subSequence(min, max); editText.setText(text.subSequence(0, min) + "シャバドゥビタッチ" + convertKanaFull2Half(sub) + text.subSequence(max, text.length())); return true; } return false; } }); } private static final char[] FULL_WIDTH_KANA = { 'ァ', 'ア', 'ィ', 'イ', 'ゥ', 'ウ', 'ェ', 'エ', 'ォ', 'オ', 'カ', 'ガ', 'キ', 'ギ', 'ク', 'グ', 'ケ', 'ゲ', 'コ', 'ゴ', 'サ', 'ザ', 'シ', 'ジ', 'ス', 'ズ', 'セ', 'ゼ', 'ソ', 'ゾ', 'タ', 'ダ', 'チ', 'ヂ', 'ッ', 'ツ', 'ヅ', 'テ', 'デ', 'ト', 'ド', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'バ', 'パ', 'ヒ', 'ビ', 'ピ', 'フ', 'ブ', 'プ', 'ヘ', 'ベ', 'ペ', 'ホ', 'ボ', 'ポ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ャ', 'ヤ', 'ュ', 'ユ', 'ョ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ヮ', 'ワ', 'ヰ', 'ヱ', 'ヲ', 'ン', 'ヴ', 'ヵ', 'ヶ'}; private static final String[] HALF_WIDTH_KANA = { "ァ", "ア", "ィ", "イ", "ゥ", "ウ", "ェ", "エ", "ォ", "オ", "カ", "ガ", "キ", "ギ", "ク", "グ", "ケ", "ゲ", "コ", "ゴ", "サ", "ザ", "シ", "ジ", "ス", "ズ", "セ", "ゼ", "ソ", "ゾ", "タ", "ダ", "チ", "ヂ", "ッ", "ツ", "ヅ", "テ", "デ", "ト", "ド", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "バ", "パ", "ヒ", "ビ", "ピ", "フ", "ブ", "プ", "ヘ", "ベ", "ペ", "ホ", "ボ", "ポ", "マ", "ミ", "ム", "メ", "モ", "ャ", "ヤ", "ュ", "ユ", "ョ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ワ", "イ", "エ", "ヲ", "ン", "ヴ", "カ", "ケ"}; private static final char FULL_WIDTH_FIRST = FULL_WIDTH_KANA[0]; private static final char FULL_WIDTH_LAST = FULL_WIDTH_KANA[FULL_WIDTH_KANA.length - 1]; public static String convertKanaFull2Half(char c) { if (c >= FULL_WIDTH_FIRST && c <= FULL_WIDTH_LAST) { return HALF_WIDTH_KANA[c - FULL_WIDTH_FIRST]; } else if(c == 'ー') { return "-"; } else { return String.valueOf(c); } } public static String convertKanaFull2Half(CharSequence cs) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < cs.length(); i++) { sb.append(convertKanaFull2Half(cs.charAt(i))); } return sb.toString(); } }





*1 仮面ライダーウィザードでぐぐってください。





0 件のコメント:

コメントを投稿