AndroidのActionModeで文章選択時に翻訳や任意の処理を行う

[mixiグループアドベントカレンダー](http://qiita.com/advent-calendar/2015/mixi)3日目です。

AndroidのmixiアプリでActionModeを使ったTextViewの文章コピーを実現しようとして諦めた知見を書きます。

# ActionModeとは

AndroidでEditTextやTextViewを長押ししていると出てくる、文章のコピー・切り取り・貼り付け・全て選択のメニューがある領域のことです。
Android 6.0未満では、ActionBarが表示されている領域にメニューが表示されます。
Android 6.0からは表示方法が変更され、選択部分の上に表示されるようになりました。

![device-2015-12-02-152728.png](https://qiita-image-store.s3.amazonaws.com/0/75828/57eaeb17-631d-0095-6d5e-8345b9dc28be.png)

# 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
<?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の実装を入れます。

```java
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します。

```java
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    mode.getMenuInflater().inflate(R.menu.menu_edittext, menu);
    return true;
}
```

ちなみにAndroid側で既に用意されているコピー・切り取り・貼り付け・全て選択を消したい場合もここで消すことができます。

```java
@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が取れるので、これをもとにどのアイテムがタップされたか判断します。

```java
@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で文章の一部を取得します。

```java
EditText editText = (EditText) findViewById(R.id.editText);
int selectionStart = editText.getSelectionStart();
int selectionEnd = editText.getSelectionEnd();
CharSequence selectedText = editText.getText().subSequence(selectionStart, selectionEnd);
```

以下、実装例です。

* MainActivity.java

```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) ）

```java
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には表示されませんが、自分で実装することで追加可能です。

![device-2015-12-03-130818.png](https://qiita-image-store.s3.amazonaws.com/0/75828/8ed596b7-6280-9ed1-46bf-0b39d77fc9b4.png)

まず、アイテムIDをフィールドに定義しておきます

```java
private int PROCESS_TEXT_ITEM_ID = 1234;
```

次に翻訳メニューをActionModeに追加します。追加するタイミングはonCreateActionMode時です。
Intent.ACTION_PROCESS_TEXTというActionをサポートしたActivityを探し、メニューに追加します。
この時、一緒にメニュー選択時に実行されるIntentも作成しておきます。

```java
@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というキーで翻訳したい文字列を追加します。

```java
@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にも実装されています。

```xml
<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" />
```

ただし、１つ問題があり
URLSpan or Linkify.addLinksを使っているTextViewで問題があり、リンクをタップしてもリンクに飛ばなくなります。原因は不明・・・

結果mixiアプリに導入できませんでした。

# トラブルシューティング

## Toolbar使用時にActionMode表示ができない

Androidのmixiアプリでは、android.support.v7.widget.Toolbarを利用していますが、ActionModeがうまく表示されません。
解消するには
テーマのwindowActionModeOverlayをtrueにします。

```xml
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:windowActionModeOverlay">true</item>
</style>
```

そして、AppCompatActivity#setSupportActionBarを呼んで下さい。
（android.widget.Toolbarの場合はsetActionbar）

```java
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
```


次は[kamasu](http://qiita.com/kamasu)さんがUnityEditorでAIシステムを作った話について書く予定です。