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;
}
}
}
Believe it or not, we're only halfway finished. The above code takes care of removing the selection when the CAB closes. To close the CAB on a touch event, we have to do a little more work. This time it's much more straightforward. I use a
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);
}
}
And that should do it! We did it!
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. :)