Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Webview text selection not clearing

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.

Upon long-press, selection highlight appears along with the contextual action bar.

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.

Selecting a button, or touching outside the selection area.The CAB should close, 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.

Tap a button from the contextual action bar.After tapping a button, the selection remains.

When I tap outside the selection, just the opposite happens. The selection is removed, but the contextual action bar remains on the screen.

Tapping the <code>WebView</code> after the selection starts.The selection is cleared, but the contextual action bar remains.


Here is the code for my 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?

like image 428
Sean Beach Avatar asked Apr 24 '14 18:04

Sean Beach


1 Answers

After numerous attempts at solving this, I have finally got it!

The important thing to realize is that WebViews 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. :)

like image 182
Sean Beach Avatar answered Nov 19 '22 21:11

Sean Beach