LoginSignup
26
27

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-12-03

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

26
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
27