📜 ⬆️ ⬇️

Floating toolbar for text selection in Android Marshmallow: parsing innovations


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

In preparation for speaking at the GDG conference in Nizhny Novgorod, I discovered that this new feature is extremely poorly documented, the only available article is not entirely true, and there are vanishingly few examples on the Internet of using this feature. 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 appeared evolutionarily, it’s easiest to start the story “from the stove”. Developers with experience can boldly flip right through to the “New” section without any risk of missing anything. If that - then come back. Younger developers can find useful examples in practice under spoilers.

Old


The first. Vulgaris menu


The story about the oldest regular menu. With examples and screenshots
The menus in Android are quite similar: they are described using XML , then at the right time the corresponding objects are created in memory according to this description using MenuInflater , this menu is displayed, and information about clicking on the corresponding element comes to callback.
')
The usual menu, which at times was called by pressing the hardware key “Menu”, is described in the application resources as an XML file.

<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/action_foo" android:orderInCategory="100" android:title="@string/word_foo"/> <item android:id="@+id/action_foobar" android:orderInCategory="101" android:title="@string/word_bar"/> <item android:id="@+id/action_baz" android:orderInCategory="102" android:title="@string/word_baz"/> </menu> 

Menu items will be sorted by field android: orderInCategory.

The menu is created by invoking 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 on the selection of 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 following menu:



Everything is simple and familiar.

The second. Context menu


The story about the context menu. With examples and screenshots
The context menu is a floating menu that appears when you long click on any element of the interface. This element must be previously 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 we leave it outside the article. Examples will be without them.

Consider a simple example.

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

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

Then everything is similar to the previous item, 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 click on the element with the text context menu appears.

Screenshot



Third. Menu in AppBar and Toolbar


The story about the menu with the Toolbar. With examples and screenshots
Starting with Android Honeycomb (aka 3.0, it’s SDK 11, that is, also quite a long time ago), the hardware Menu button was abolished, and the menu was displayed in the “action bar”, which is also the “application bar”. It was possible to display some of the menu items in this line as icons, and leave some of them hidden before clicking on the menu icon to the right.

Fields for describing icons and locations added XML with a description of the menu. Otherwise, it remains the same menu with the known onCreateOptionsMenu () and onOptionsItemSelected () .

Menu with icons and location. Added android: icon and android: orderInCategory
 <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_foo" android:icon="@android:drawable/ic_media_previous" android:orderInCategory="100" android:title="@string/word_foo" app:showAsAction="ifRoom" /> <item android:id="@+id/action_foobar" android:icon="@android:drawable/ic_media_next" android:orderInCategory="101" android:title="@string/word_bar" app:showAsAction="ifRoom" /> <item android:id="@+id/action_baz" android:icon="@android:drawable/ic_media_pause" android:orderInCategory="102" android:title="@string/word_baz" app:showAsAction="never"/> </menu> 



Visible part of the menu

Hidden part of the menu
if you click on the "three points"


At this level, people usually know about the menu after the first newcomer book. Thank you for having enough patience to read this far, it will be more interesting further.


Fourth. Contextual action mode


Story about contextual action mode. With examples and screenshots
The context action mode, also known as the “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 you can "choose." Take for example ToggleButton . This mode the application activates itself, respectively, do it when the status of the ToggleButton 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 remains 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); } }; 

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

The selection of the user 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 should obviously differ from the main one, so we will create another XML with a description.

XML with menu description. Same in structure, but with other points
 <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_camera" android:icon="@android:drawable/ic_menu_camera" android:orderInCategory="100" android:title="@string/word_camera" app:showAsAction="ifRoom" /> <item android:id="@+id/action_compass" android:icon="@android:drawable/ic_menu_compass" android:orderInCategory="101" android:title="@string/word_compass" app:showAsAction="ifRoom" /> </menu> 


We get this behavior:



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


New


In Android Marshmallow (aka 6.0, aka SDK 23), two innovations appeared in the menu. Both of these innovations are not yet supported by the Support Library and only work on devices with SDK 23. Therefore, before calling the methods that appeared, 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.

The fifth. Another context menu


The method for creating the “Context Action Mode” (described above) has been extended. The second parameter startActionMode () can pass a constant that specifies 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.

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



No icons. Location - horizontal.

If there are so many points 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).

The sixth. Expansion of the menu when selecting text


And finally, what was promised in the article on innovations in Android Marshmallow . Now you can add a menu to the menu that appears when you select text. The animated picture of this miracle was at the very beginning of the article, but if you want, it is again under the spoiler.

Animated picture from the menu


Implementation


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

Further, for all elements in which we want to expand the menu when editing, you 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. Separated them because not all actions make sense when nothing is selected, and, accordingly, the contents of the appearing menu should be different.



On an empty EditText it looks like this:




Oh. Where did my menu go?


Look at the screenshots below. Any idea where all the added menu items are?



A hint can be found here:



I will explain. It is likely that the remaining items will not fit in the second, vertical part of the menu either. And there will be a scroll. In the screenshots above it happened.

The problem is that the user doesn’t see any signs of scrolling: the scrolling icon disappears almost immediately, and the edge of the unclaimed item doesn’t look. I got the 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 there is another problem: the menu does not expand with fullscreen input mode as described above. I found only one workout: turn off fullscreen mode using android: imeOptions = "flagNoExtractUi".

How to place your item before the standard?


Native menu items have order parameters from 1 to 5. Therefore, using the android: orderInCategory in the menu description, you cannot set the position before the native items. But you can change the order of 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, an article about Android Marshmallow updates .

I reread this place several dozen times, but I could not relate what is written there to 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 creates setCustomSelectionActionModeCallback () .



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



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

If you look at android.widget.Editor , then the mCustomSelectionActionModeCallback field is also of type ActionMode.Callback :



And nowhere in this code is it expected that custom callback will be of type ActionMode.Callback2 .
android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/Editor.java

The article continues to talk hard about how to use ActionMode.Callback2 :



My guess is that a fragment of some internal documentation on ActionMode.Callback2 got into the article due to an editor's oversight. Do you have any other hypothesis, write about it.

Example


The entire code from this article can be found on GitHub .

Source: https://habr.com/ru/post/271737/


All Articles