mixiグループアドベントカレンダー3日目です。
AndroidのmixiアプリでActionModeを使ったTextViewの文章コピーを実現しようとして諦めた知見を書きます。
ActionModeとは
AndroidでEditTextやTextViewを長押ししていると出てくる、文章のコピー・切り取り・貼り付け・全て選択のメニューがある領域のことです。
Android 6.0未満では、ActionBarが表示されている領域にメニューが表示されます。
Android 6.0からは表示方法が変更され、選択部分の上に表示されるようになりました。
ActionModeに独自のメニューを表示する
メニューの定義
ActionModeに表示したいアイテムを定義するためにメニューリソースを用意します。Actionbarにメニューアイテムを定義する要領と同じです。
注意が必要なのが、必ずandroid:showAsAction="always"にしておくことです。
定義がない場合、右上・・・のメニューにアイテムが入りますが、・・・をタップした途端にActionModeが消えてしまい、アイテムを選択できなくなります。
Android 6.0 未満では、android:iconで指定されたリソースが優先的にメニューに表示されますが、
Android 6.0 では、android:titleが優先的に表示されます。だたし、android:iconだけ定義した場合でもきちんとFloatingActionModeにアイコンだけが表示されました。
- res/menu/menu_edittext.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_close"
android:title="Close"
android:showAsAction="always"/>
<item android:id="@+id/action_toast"
android:title="Toast"
android:icon="@mipmap/ic_launcher"
android:showAsAction="always"/>
</menu>
EditTextへの設定
次に表示させるActivity内で、対象のEditTextに対してActionModeの設定を行います。
EditText#setCustomSelectionActionModeCallbackの引数としてActionMode.Callbackの実装を入れます。
EditText editText = (EditText) findViewById(R.id.editText);
editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// ActionModeが表示されるときに呼ばれます。
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// onCreateActionMode後に呼ばれます。
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// アイテムをクリックしたときに呼ばれます。
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// ActionModeが閉じたタイミングでonDestroyActionModeが呼ばれます。
}
});
onCreateActionModeでメニューリソースをinflateします。
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.menu_edittext, menu);
return true;
}
ちなみにAndroid側で既に用意されているコピー・切り取り・貼り付け・全て選択を消したい場合もここで消すことができます。
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
menu.removeItem(android.R.id.copy); // コピーの削除
menu.removeItem(android.R.id.cut); // 切り取りの削除
menu.removeItem(android.R.id.paste); // 貼り付けの削除
menu.removeItem(android.R.id.selectAll); // 全て選択の削除
mode.getMenuInflater().inflate(R.menu.menu_edittext, menu);
return true;
}
アイテムがタップされた際は、onActionItemClickedが呼ばれます。
item.getItemId()でタップされたアイテムのIDが取れるので、これをもとにどのアイテムがタップされたか判断します。
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.action_toast) {
// 処理
return true;
}
if (item.getItemId() == R.id.action_close) {
mode.finish(); // 閉じる
return true;
}
return false;
}
ActionModeを閉じるには、ActionMode#finish()を呼びます。そのままだとタップ後自動で閉じません。
選択された文章の取得には、EditText#getSelectionStart()/getSelectionEnd()を使います。選択された位置の始めと終わりを取得できます。
これをもとにsubSequenceで文章の一部を取得します。
EditText editText = (EditText) findViewById(R.id.editText);
int selectionStart = editText.getSelectionStart();
int selectionEnd = editText.getSelectionEnd();
CharSequence selectedText = editText.getText().subSequence(selectionStart, selectionEnd);
以下、実装例です。
- MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText editText = (EditText) findViewById(R.id.editText);
editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.menu_edittext, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.action_toast) {
EditText editText = (EditText) findViewById(R.id.editText);
int selectionStart = editText.getSelectionStart();
int selectionEnd = editText.getSelectionEnd();
CharSequence selectedText = editText.getText().subSequence(selectionStart, selectionEnd);
Toast.makeText(getApplicationContext(), selectedText + "が選択されています", Toast.LENGTH_SHORT).show();
return true;
}
if (item.getItemId() == R.id.action_close) {
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
Log.d(TAG, "onDestroyActionMode");
}
});
}
}
何も選択されていない場合のメニュー(Android 6.0以降のみ)
Android 6.0 からは、EditTextでカーソル上に文章が無い場合のActionModeのカスタマイズがサポートされています。
(6.0未満では貼り付けしか表示されません)
こちらのActionModeはEditText#setCustomInsertionActionModeCallbackで設定できます。
過去のバージョン対応を考え、Android 6.0で実行される場合のみ動くようにすると良いです。( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
editText.setCustomInsertionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.menu_insertion, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.action_insert) {
EditText editText = (EditText) findViewById(R.id.editText);
editText.append("Hoge");
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
});
}
翻訳メニューを入れる
Google翻訳のアプリをインストールしてある場合、Android 6.0からActionModeに自動的に翻訳メニューが追加されるようになりました。
Android6.0未満ではActionModeには表示されませんが、自分で実装することで追加可能です。
まず、アイテムIDをフィールドに定義しておきます
private int PROCESS_TEXT_ITEM_ID = 1234;
次に翻訳メニューをActionModeに追加します。追加するタイミングはonCreateActionMode時です。
Intent.ACTION_PROCESS_TEXTというActionをサポートしたActivityを探し、メニューに追加します。
この時、一緒にメニュー選択時に実行されるIntentも作成しておきます。
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Intent.ACTION_PROCESS_TEXTをサポートする画面を探します
Intent intent = new Intent(Intent.ACTION_PROCESS_TEXT);
intent.setType("text/plain");
PackageManager manager = getPackageManager();
List<ResolveInfo> activities = manager.queryIntentActivities(intent, 0);
// メニューに追加します
int order = 100;
for (ResolveInfo resolveInfo : activities) {
MenuItem item = menu.add(Menu.NONE, PROCESS_TEXT_ITEM_ID, order++, resolveInfo.loadLabel(manager));
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// メニュー選択時のIntentを作成します
Intent menuIntent = new Intent(Intent.ACTION_PROCESS_TEXT);
menuIntent.setType("text/plain");
menuIntent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
item.setIntent(menuIntent);
}
return true;
}
最後、メニュー選択時に先ほど作成したIntentに選択された文字列を添付しておきます。
Intent.EXTRA_PROCESS_TEXTというキーで翻訳したい文字列を追加します。
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == PROCESS_TEXT_ITEM_ID) {
EditText editText = (EditText) findViewById(R.id.editText);
int selectionStart = editText.getSelectionStart();
int selectionEnd = editText.getSelectionEnd();
CharSequence selectedText = editText.getText().subSequence(selectionStart, selectionEnd);
// 選択された文字列をIntentに添付します。
item.getIntent().putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
}
return false;
}
以上で、「翻訳」というアイテムが追加されているはずです。
プログラマチックにActionModeを表示させたい
Activity#startActionMode / AppCompatActivity#startSupportActionMode を利用します。
TextViewで文章選択を有効にする
android:textIsSelectableをtrueにすると、有効になります。
setCustomSelectionActionModeCallback等はEditTextと同様にTextViewにも実装されています。
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is a sample text!"
android:textIsSelectable="true" />
ただし、1つ問題があり
URLSpan or Linkify.addLinksを使っているTextViewで問題があり、リンクをタップしてもリンクに飛ばなくなります。原因は不明・・・
結果mixiアプリに導入できませんでした。
トラブルシューティング
Toolbar使用時にActionMode表示ができない
Androidのmixiアプリでは、android.support.v7.widget.Toolbarを利用していますが、ActionModeがうまく表示されません。
解消するには
テーマのwindowActionModeOverlayをtrueにします。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowActionModeOverlay">true</item>
</style>
そして、AppCompatActivity#setSupportActionBarを呼んで下さい。
(android.widget.Toolbarの場合はsetActionbar)
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
次はkamasuさんがUnityEditorでAIシステムを作った話について書く予定です。