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

  • 21
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

mixiグループアドベントカレンダー3日目です。

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

ActionModeとは

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

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

device-2015-12-03-130818.png

まず、アイテム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システムを作った話について書く予定です。