I have implemented an ActionMode.Callback
for custom text selection functions within a WebView
. The problem that I am having is that the selection and the action mode states do not match.
When I long-press, everything starts out just fine.
When I interact with one of the buttons, or the WebView
(excluding the actual selection) then the ActionMode
should be destroyed, and the selection should disappear.
In Android 4.4, KitKat, this is exactly what happens.
However, this is not what is happening in 4.1.1 - 4.3, Jelly Bean. When I click one of the buttons, the selection is not removed.
When I tap outside the selection, just the opposite happens. The selection is removed, but the contextual action bar remains on the screen.
CustomWebView
public class CustomWebView extends WebView {
private ActionMode.Callback mActionModeCallback;
@Override
public ActionMode startActionMode(Callback callback) {
ViewParent parent = getParent();
if (parent == null) {
return null;
}
mActionModeCallback = new CustomActionModeCallback();
return parent.startActionModeForChild(this, mActionModeCallback);
}
private class CustomActionModeCallback implements ActionMode.Callback {
// Called when the action mode is created; startActionMode() was called
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.contextual_menu, menu);
return true;
}
// Called each time the action mode is shown.
// Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// This method is called when the handlebars are moved.
loadJavascript("javascript:getSelectedTextInfo()");
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch(item.getItemId() {
case R.id.button_1:
// do stuff
break;
...
default:
break;
}
mode.finish(); // Action picked, so close the CAB
return true;
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
// TODO This does not work in Jelly Bean (API 16 - 18; 4.1.1 - 4.3).
clearFocus(); // Remove the selection highlight and handles.
}
}
}
As the comment above shows, I believe the problem is with the clearFocus()
method. When I remove that method, pressing a button leaves the selection behind in 4.4, just like the behavior in Jelly Bean. clearFocus()
gives the expected behavior in 4.4, but is not transferring to earlier APIs. (Do note that clearFocus()
is not new to KitKat; it has been in Android since API 1.)
How can this be fixed?
After numerous attempts at solving this, I have finally got it!
The important thing to realize is that WebView
s before Android 4.4 (KitKat) are different from your typical browser. There are a few hidden classes that come into play that start to mess up things. There's WebViewCore
which does all the heavy lifting and actually produces results, and there's WebViewClassic
, which is the culprit of this problem.
The solution is a semi-hack, as you don't really have to do anything to manipulate the underlying classes, but you do have to catch the problem scenarios.
WebViewClassic
takes care of intercepting long presses and handling them for text selection, including the animation of the selection highlight and the selection handles, as well as starting the ActionMode
that populates the Contextual Action Bar (CAB). Unfortunately, since we want to override that ActionMode
with our own, the text selection and the CAB become out of sync, because they are not associated with each other. To solve this, keep track of your own custom ActionMode.Callback
, as well as the ActionMode.Callback
associated with the selection animation. Then, when your ActionMode.Callback
is destroyed, call the selection's finish()
method to destroy that ActionMode.Callback
, as well.
OK, enough talk; here's the code.
public class CustomWebView extends WebView {
private ActionMode mActionMode;
private ActionMode.Callback mActionModeCallback;
// Add this class variable
private ActionMode.Callback mSelectActionModeCallback;
@Override
public ActionMode startActionMode(Callback callback) {
/* When running Ice Cream Sandwich (4.0) or Jelly Bean (4.1 - 4.3), there
* is a hidden class called 'WebViewClassic' that draws the selection.
* In order to clear the selection, save the callback from Classic
* so it can be destroyed later.
*/
// Check the class name because WebViewClassic.SelectActionModeCallback
// is not public API.
String name = callback.getClass().toString();
if (name.contains("SelectActionModeCallback")) {
mSelectActionModeCallback = callback;
}
mActionModeCallback = new CustomActionModeCallback();
// We haven't actually done anything yet. Send our custom callback
// to the superclass so it will be shown on screen.
return super.startActionModeForChild(this, mActionModeCallback);
}
private class CustomActionModeCallback implements ActionMode.Callback {
// Called when the action mode is created; startActionMode() was called
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// This is important for part 2.
mActionMode = mode;
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.contextual_menu, menu);
return true;
}
// Called each time the action mode is shown.
// Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// This method is called when the handlebars are moved.
loadJavascript("javascript:getSelectedTextInfo()");
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch(item.getItemId() {
case R.id.button_1:
// do stuff
break;
...
default:
break;
}
mode.finish(); // Action picked, so close the CAB
return true;
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
clearFocus(); // Remove the selection highlight and handles.
// Semi-hack in order to clear the selection
// when running Android earlier than KitKat.
if (mSelectActionModeCallback != null) {
mSelectActionModeCallback.onDestroyActionMode(mode);
}
// Relevant to part 2.
mActionMode = null;
}
}
}
GestureDetector
and listen for a single tap event. When I get that event, I call finish()
on mActionMode
to close the CAB:
public class CustomWebView extends WebView {
private ActionMode mActionMode;
private ActionMode.Callback mActionModeCallback;
private ActionMode.Callback mSelectActionModeCallback;
// Code from above segment
...
private class CustomGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (mActionMode != null) {
mActionMode.finish();
return true;
}
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Send the event to our gesture detector
// If it is implemented, there will be a return value
this.mDetector.onTouchEvent(event);
// If the detected gesture is unimplemented, send it to the superclass
return super.onTouchEvent(event);
}
}
You will not find the WebViewClassic
material anywhere else; that's why I provided so much detail as to what's happening. It took many hours with the debugger to figure out what was going on. Fortunately, the GestureDetector
class is well-documented, and includes multiple tutorials. I got my information from the Android Developers website. I hope this helped those of you that struggled with this problem as much as I did. :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With