Use a custom contextual action bar for WebView text selection
Asked Answered
S

2

27

I have used this guide from Google and this tutorial to produce my own contextual action bar.

private ActionMode.Callback mActionModeCallback = new 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.annotation_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) {
        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.custom_button:
                // do some stuff
                break;
            case R.id.custom_button2:
                // do some other stuff
                break;
            default:
                // This essentially acts as a catch statement
                // If none of the other cases are true, return false
                // because the action was not handled
                return false;
        }
        finish(); // An action was handled, so close the CAB
        return true;
    }

    // Called when the user exits the action mode
    @Override
    public void onDestroyActionMode(ActionMode mode) {
        mActionMode = null;
    }
};

This menu is designed to appear when the user selects text, so it overrides the native copy/paste menu. Now I get to my issue.

Because I am overriding functions for text selection, I also added a LongClickListener to a WebView and implemented the onLongClick(View v) method so I can detect when users make the selection.

    myWebView.setOnLongClickListener(new View.OnLongClickListener() {

        @Override
        public boolean onLongClick(View v) {
            if (mActionMode != null) {
                return false;
            }

            mActionMode = startActionMode(mActionModeCallback);
            v.setSelected(true);
            return true;
        }
    });

When I long click, I see my custom menu appear, but no text is highlighted.
I need to have the text selection functionality; without it, my menu is pointless.

How do I override onLongClick(View v), but maintain the text selection provided by Android?
If that is not possible, can I make the call to startActionMode(mActionModeCallback) somewhere else so that text will be selected as normal, but my custom menu will also appear?
If neither of those are possible... help.

Sokil answered 11/3, 2014 at 21:22 Comment(0)
S
42

THERE IS AN EASIER WAY! See update below :D


For the sake of completeness, here is how I fixed the problem:

I followed the suggestion according to this answer, with a little more tweaking to more closely match the overridden code:

public class MyWebView extends WebView {

    private ActionMode mActionMode;
    private mActionMode.Callback mActionModeCallback;

    @Override
    public ActionMode startActionMode(Callback callback) {
        ViewParent parent = getParent();
        if (parent == null) {
            return null;
        }
        mActionModeCallback = new CustomActionModeCallback();
        return parent.startActionModeForChild(this, mActionModeCallback);
    }
}

Essentially, this forces your customized CAB to appear instead of the Android CAB. Now you have to modify your callback so that the text highlight will go away along with the CAB:

public class MyWebView extends WebView {
    ...
    private class CustomActionModeCallback implements ActionMode.Callback {
        ...
        // Everything up to this point is the same as in the question

        // Called when the user exits the action mode
        @Override
        public void onDestroyActionMode(ActionMode mode) {
            clearFocus(); // This is the new code to remove the text highlight
             mActionMode = null;
        }
    }
}

That's all there is to it. Be aware that as long as you are using MyWebView with the overridden startActionMode there is NO WAY to get the native CAB (the copy/paste menu, in the case of a WebView). It may be possible to implement that sort of behavior, but that is not the way this code works.


UPDATE: There is a much easier way to do this! The above solution works well, but here is an alternative, easier way.

This solution provides less control over the ActionMode, but it requires far less code than the above solution.

public class MyActivity extends Activity {

    private ActionMode mActionMode = null;

    @Override
    public void onActionModeStarted(ActionMode mode) {
        if (mActionMode == null) {
            mActionMode = mode;
            Menu menu = mode.getMenu();
            // Remove the default menu items (select all, copy, paste, search)
            menu.clear();

            // If you want to keep any of the defaults,
            // remove the items you don't want individually:
            // menu.removeItem(android.R.id.[id_of_item_to_remove])

            // Inflate your own menu items
            mode.getMenuInflater().inflate(R.menu.my_custom_menu, menu);
        }

        super.onActionModeStarted(mode);
    }

    // This method is what you should set as your item's onClick
    // <item android:onClick="onContextualMenuItemClicked" />
    public void onContextualMenuItemClicked(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.example_item_1:
                // do some stuff
                break;
            case R.id.example_item_2:
                // do some different stuff
                break;
            default:
                // ...
                break;
        }

        // This will likely always be true, but check it anyway, just in case
        if (mActionMode != null) {
            mActionMode.finish();
        }
    }

    @Override
    public void onActionModeFinished(ActionMode mode) {
        mActionMode = null;
        super.onActionModeFinished(mode);
    }
}

Here is an example Menu to get you started:

<!-- my_custom_menu.xml -->
<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/example_item_1"
        android:icon="@drawable/ic_menu_example_1"
        android:showAsAction="always"
        android:onClick="onContextualMenuItemClicked"
        android:title="@string/example_1">
    </item>

    <item
        android:id="@+id/example_item_2"
        android:icon="@drawable/ic_menu_example_2"
        android:showAsAction="ifRoom"
        android:onClick="onContextualMenuItemClicked"
        android:title="@string/example_2">
    </item>

</menu>

That's it! You're done! Now your custom menu will show up, you don't have to worry about the selection, and you barely have to concern yourself with the ActionMode lifecycle.

This works nearly flawlessly with a WebView that occupies its entire parent Activity. I am not sure how well it will work if there are multiple Views within your Activity at one time. It will likely require some tweaking in that case.

Sokil answered 13/3, 2014 at 21:20 Comment(21)
I did domethihg like your solution, but my onDestroyActionMode is not called. Did face some problem like this?Novelty
What version Android are you using? In versions earlier than 4.4 (KitKat) yes, I experienced the same problem. It is due to a hidden inner class called WebViewClassic. In that case, you have to do some trickery. It's complicated, so I'll await your reply before I post a huge comment about it.Sokil
Before I go too in-depth with the WebViewClassic work around: are you calling finish() when an action item is selected? I realized the code from my question is slightly off; I have modified it. (Take a look at the edited onActionItemClicked method.)Sokil
Yes. I call mode.finish().Novelty
I apologize, for I have mislead you: I don't have the answer to your question. I have asked the question here: #23277979 but no answers have come. Again, sorry about that.Sokil
How can I get the selected text and bundle it into a string? The text here is highlighted but I don't know how to get that text as in case the default Copy button.Victory
You need to write your own Javascript to access the actual text. Unfortunately, Google has not yet provided a public API for that.Sokil
If so, I'm thinking of utilising the default Copy function, then set a listener to the clipboard change...but I don't want the name Copy. The question is then can Copy of the CAB be renamed?Victory
Yes, it can. Get a reference to the Copy button using MenuItem copy = menu.findItem(android.R.id.[id_for_copy]); then change the title by calling copy.setTitle("Your desired title");. I think the id is simply android.R.id.copy but I am not sure about that.Sokil
// This method is what you should set as your item's onClick // <item android:onClick="onMenuItemClicked" /> public void onContextualMenuItemClicked(MenuItem item) {... Neat answer, but typo on "onMenuItemClicked" --> "onContextualMenuItemClicked"Larimer
Good catch. I have edited accordingly. In the future, you can be more proactive by simply editing yourself, rather than commenting. Editing significant typos in questions, such as this one, is just part of being a good SO citizen. :)Sokil
Hello, is it imposible if show action mode in webview added by service ?Starstarboard
No, that is an Android ViewGroup method developer.android.com/reference/android/view/…Sokil
I'd like to keep the Copy, Paste and Cut menu items but it seems the menu IDs for these are different on each device. I've worked around this by checking menuItem.getTitle().equals("Copy") etc. Is there a better way to do this. This seems hacky.Frisk
In marshamallow , its showing default context menu during selection of text. So how to overcome this problem ? please help me outDomela
I've been trying to figure this out for a while. I basically need to only add my menu item to the existing menu. I can easily do this, but it adds it to the back of the menu (that is you have to tap on the overflow menu item before being able to see it). I want to add it to the front, but I can't seem to find a way to do this. Has anyone done this?Frisk
It sounds like you are simply calling menu.add(yourButton). By default, that adds yourButton to the end of the Menu. If you want to keep all the default options while adding your own to the beginning of the menu, try passing an order when adding your item. developer.android.com/reference/android/view/Menu.html#add(int, int, int, java.lang.CharSequence)Sokil
this line android:onClick="onContextualMenuItemClicked" is causing a crashing to the app, on launching the menu.Isochroous
This will not work, if you select more text(i.e scroll select text cursor) then all default menu appears again more then android version M.Checky
onContextualMenuItemClicked must return boolean instead of void? I'm getting Couldn't resolve menu item onClick handler onContextualMenuItemClickedUnsuspecting
onContextualMenuItemClicked part gives error. Please see this answer to assign callbacks properly to menu items. [https://mcmap.net/q/247539/-horizontal-menu-inflater-on-long-click-for-web-view]Datary
N
1

The way I did something similar was to only override the onTouchListener and to invoke a GestureDetector to detect when the WebView was long-pressed and do what I wanted from there. Here's some sample code that allows you to catch long-press events without sacrificing text-selection in the WebView. Hopefully this helps.

@Override
protected void onCreate(Bundle savedInstanceState) {
    WebView mWebView = (WebView) findViewById(R.id.myWebView);
    GestureDetector mGestureDetector = new GestureDetector(this, new CustomGestureListener());
    mWebView.setOnTouchListener(new OnTouchListener(){
        @Override
        public boolean onTouch(View view, MotionEvent arg1) {

            //Suggestion #1 - this just lets the touch to be handled by the system but allows you to detect long presses
            mGestureDetector.onTouchEvent(arg1);
            return false;

            //Suggestion #2 - this code will only let the touch be handled by the system if you don't detect a long press
            return mGestureDetector.onTouchEvent(arg1);
        }
    });
}

private class CustomGestureListener extends SimpleOnGestureListener {

    @Override
    public void onLongPress(MotionEvent e) {
        //do stuff
    }

}
Newsmagazine answered 11/3, 2014 at 23:27 Comment(5)
Your suggestion is not wrong; that does allow additional functionality for a long press. However, it does not accomplish what I am trying to do. Even when I put startActionMode(mActionModeCallback) in the onLongPress call, the native context menu still appears. I want to override said menu, while maintaining the text selection.Sokil
Ah, that makes it more clear. I thought I might have slightly misunderstood what you were asking. This question seems pretty similar to yours and there isn't an accepted solution yet #17024703Newsmagazine
But this question shows that there is a way to override what shows for copy and paste #15370547Newsmagazine
I edited my answer with something that might work. Try returning true from onTouch if a long press is detected. I haven't used that before so I'm not sure it will work like you want it toNewsmagazine
I followed the suggestions from the second link you posted and the text selection now works. However, I now have a different problem. (I will post a new question for that.)Sokil

© 2022 - 2024 — McMap. All rights reserved.