Back to Home

Floating toolbar for text selection on Android Marshmallow: parsing innovations

android · android · development · android marshmallow · android development · android sdk · mobile applications · new version

Floating toolbar for text selection on Android Marshmallow: parsing innovations


In Andriod, when you select text, a menu appears with actions that you can perform: “Cut”, “Copy”, “Send”. Android Marshmallow (SDK 23) has the opportunity to expand this menu and give the user easy access to additional features when working with text: “Translate”, “Comment”, “Quote”.

In the process of preparing for my presentation at the GDG conference in Nizhny Novgorod, I discovered that this new opportunity is extremely poorly documented, the only available article is not entirely true, and there are vanishingly few examples of using this opportunity on the web. I had to figure it out myself. The results of the study and I want to share. This can save you a lot of time.

Since various menus in Android have appeared evolutionarily, the easiest way to start a story is “from the stove”. Developers with experience can safely flip directly to the "New" section without risking missing something. If anything - then come back. Younger developers can find practical examples under spoilers.

Old


The first one. Menu vulgaris


A story about the oldest regular menu. With examples and screenshots
The menus in Android are organized quite the same way: they are described using XML , then at the right time in accordance with this description , the corresponding objects are created in memory using the MenuInflater , this menu is displayed, and information about clicking on the corresponding item comes to the callback.

The usual menu, which at times it was called up by pressing the hardware key "Menu", is described in the application resources as an XML file.


Menu items will be sorted by android: orderInCategory field.

A menu is created by calling the inflater in the onCreateOptionsMenu () method .

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.my_menu, menu);
    return true;
}

Information about choosing a menu item falls into the onOptionsItemSelected () method .

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle item selection
    switch (item.getItemId()) {
        case R.id.action_foo:
            toast("foo");
            return true;
        case R.id.action_foobar:
            toast("bar");
            return true;
        case R.id.action_baz:
            toast("baz");
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

Everything described above leads to the appearance of such a menu:



Everything is simple and familiar.

The second one. Context menu


The story about the context menu. With examples and screenshots
The context menu is a floating menu that appears with a long press on any interface element. This item must be pre-registered to work with the context menu.

Context menus are most often used when working with lists. But this story would be more about lists than about menus, so let's leave it outside the scope of the article. Examples will be without them.

Consider a simple example.

You can register an element, for example, in onCreate () :

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    registerForContextMenu(findViewById(R.id.text_view_one));
}

Further, everything is similar to the previous paragraph, only inside other methods ( onCreateContextMenu () and onContextItemSelected () ):

@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.my_menu, menu);
}

@Override
public boolean onContextItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.action_foo:
            toast("foo");
            return true;
        case R.id.action_foobar:
            toast("bar");
            return true;
        case R.id.action_baz:
            toast("baz");
            return true;
        default:
            return super.onContextItemSelected(item);
    }
}

With a long press on an element with text, a context menu appears.

Screenshot



The third. Menus in AppBar and Toolbar


A story about the menu with the Toolbar. With examples and screenshots
Starting with Android Honeycomb (aka 3.0, aka SDK 11, that is, also quite a while ago) the hardware Menu button has been canceled, and the menu has appeared in the "action bar", it is also the "application bar". Now you can show some of the menu items in this line as icons, and leave some hidden until you click on the menu icon on the right.

Fields for the description of the icons and the location were supplemented by XML with a description of the menu. For the rest, this remains the same menu with the onCreateOptionsMenu () and onOptionsItemSelected () known to us .

Menu with icons and layout. Added android: icon and android: orderInCategory



Visible portion of the menu

The hidden part of the menu,
if you click on the "three points"


At this level, everyone usually knows about the menu after the first new book. Thank you for the patience to read here, it will be more interesting further.


Fourth. Contextual action mode


Story about contextual action mode. With examples and screenshots
The contextual action mode, aka “Contextual action mode” allows you to show the user a set of actions that can be performed on the selected item. Like the context menu, this tool is convenient when working with lists, but we do not consider lists in this article.

In order to consider the operation of this mode, we need an element that can be "selected". Take for example ToggleButton . The application activates this mode by itself, accordingly, we will do this when the ToggleButton status changes:

checkedListener =
        new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView,
                                         boolean isChecked)
            {
                if (isChecked) {
                    actionMode = startActionMode(actionModeCallback);
                    actionMode.setTitle("Action Mode");
                } else {
                    if (actionMode != null) {
                        actionMode.finish();
                    }
                }
            }
        };
toggleButton.setOnCheckedChangeListener(checkedListener);

actionModeCallback is an instance of the ActionMode.Callback class that contains callbacks for working with menus. The menu remained the same, all with the same good old mechanics:

actionModeCallback = new ActionMode.Callback() {
    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.my_menu_two, menu);
        return true;
    }
    ...
    @Override
    public void onDestroyActionMode(ActionMode mode) {
        actionMode = null;
        toggleButton.setChecked(false);
    }
};

Pay attention to the value that we save in the variable actionMode. This is an instance of the ActionMode class , we need it to be able to change the title ( actionMode.setTitle () ), the subtitle ( actionMode.setSubtitle () ), and also end this mode ( actionMode.finish () ). After the action is completed, the mode does not automatically end, and if we need to, then we must complete it ourselves.

The user's choice is also processed in the usual way:

actionModeCallback = new ActionMode.Callback() {
    ... 
    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_compass:
                toast("compass");
                return true;
            case R.id.action_camera:
                toast("camera");
                return true;
            default:
                return false;
        }
    }
};

The menu for contextual actions, obviously, should be different from the main one, so let's create another XML with a description.

XML with a description of the menu. Same in structure, but with different points


We get this behavior:



Everything old, that is, known at least with SDK 11 is now over, the unknown has begun.


New


In Android Marshmallow (aka 6.0, aka SDK 23), two new features appeared in the menu. Both of these innovations are not yet supported by the Support Library and work only on devices with SDK 23. Therefore, before calling the methods that appear, you need to check the SDK number of the device on which the application is running.

For the convenience of the story, we will get rid of these checks by specifying minSdkVersion 23.

Fifth. Another context menu


The method for creating a “Contextual Action Mode” (described above) has been expanded. The second parameter startActionMode () can pass a constant that sets the type of display.

The value of ActionMode.TYPE_PRIMARY corresponds to the old behavior. That is, startActionMode (actionModeCallback) and startActionMode (actionModeCallback, ActionMode.TYPE_PRIMARY) are the same thing.

If you set the type ActionMode.TYPE_FLOATING , then the menu takes the following form:



No icons. The location is horizontal.

If there are a lot of points so that they do not fit in width, then the familiar “three points” will appear:



Everything else is exactly the same as in “Contextual action mode” (see above).

Sixth. Menu extension for text selection


And finally, what was promised in the article about innovations in Android Marshmallow . Now you can supplement the menu that appears when you select text with your own items. An animated picture of this miracle was at the very beginning of the article, but if you want, it is again under the spoiler.

Animated menu picture


Implementation


To achieve this effect, you first need to create a callback inherited from ActionMode.Callback. That is, completely similar to what we created two sections above for “Contextual action mode” .

Further, for all the elements in which we want to expand the menu when editing, we need to specify this callback:

textView.setTextIsSelectable(true);
textView.setCustomSelectionActionModeCallback(actionModeCallback);
editText.setCustomSelectionActionModeCallback(actionModeCallback);
editText.setCustomInsertionActionModeCallback(actionModeCallback);

The callback specified in setCustomSelectionActionModeCallback () will be used if there is selected text. Specified in setCustomInsertionActionModeCallback () - if there is no text. They were divided because not all actions make sense when nothing is selected, and, accordingly, the contents of the menus that appear should be different.



On an empty EditText, it looks like this:




Oh. Where did my menu go?


Check out the screenshots below. Have an idea where all the added menu items have gone?



The hint can be found here: I will



explain. The probability that the remaining items will not fit in the second, vertical part of the menu is also quite high. And there will be a scroll. In the screenshots above it happened.

The problem is that the user does not see any signs of scroll: the scroll icon disappears almost immediately, and the edge of the non-positioned item does not look out. Started a bug, let's see what the creators of this feature say. code.google.com/p/android/issues/detail?id=195043

Where did my menu go again?




This time another problem: the menu with fullscreen input mode as described above does not expand. I found only one workaround: turn off fullscreen mode with android: imeOptions = "flagNoExtractUi".

How to place your item in front of the standard?


Native menu items have order parameters from 1 to 5. Therefore, using android: orderInCategory in the menu description, you cannot set the position in front of the native items. But you can change the order of the items in the already formed menu, for example, like this:

@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    // родные пункты меню "нумеруются" с 1, 
    // дополнительные я "пронумеровал" со 100
    while (menu.getItem(0).getOrder() < 100) {
        MenuItem item = menu.getItem(0);
        menu.removeItem(item.getItemId());
        // теперь родные будут "нумероваться" с 200, то есть
        // станут после дополнительных
        menu.add(item.getGroupId(), item.getItemId(),
                item.getOrder() + 200, item.getTitle());
    }
    return true;
}

We get the result:



Oddities in the documentation


There is no normal documentation for expanding the menu when selecting text. There is an article already mentioned article about the latest in Android Marshmallow .

I re-read this place several dozen times, but I could not correlate what was written there with practice. I tell in order.



If you read this article or tried it yourself, you noticed that startActionMode (callback, ActionMode.TYPE_FLOATING) does create a floating toolbar, but not at all for selection. And for selection it is created by setCustomSelectionActionModeCallback () .



The second incomprehensible instruction. The setCustomSelectionActionModeCallback () method of the android.widget.TextView class expects to receiveActionMode.Callback .



android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/TextView.java

If you look in android.widget.Editor , then the mentioned mCustomSelectionActionModeCallback field also has the type ActionMode.Callback :



And nowhere else This code is not expected to have a custom callback of type ActionMode.Callback2 .
android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/Editor.java The

article continues to persistently talk about how to use ActionMode.Callback2 :



My assumption is that the article is overlooked by the editor got a fragment of some internal documentation onActionMode.Callback2 . Do you have other hypotheses, write about it.

Example


Entire code from this article can be found on GitHub .

Read Next