Android webview override startActionMode yet keep default dismiss of text selection - android

After searching for a similar question I gave up and decided to ask myself , if there is an existing answer please notify me.
I have a WebView in which I needed to override the default actionMode , I followed suggestions from many Q & A here and everything was working well until I have noticed that something very specific was missing in the behavior of the actionMode. The dismiss event for the text selection only happens when the user presses "Back button" or "Done" on the action mode. In the Default actionMode when the user presses outside the selected text the actionMode also closes , and this I cannot achieve and not sure how can I acheive it without complicating stuff and writing extra lines of code instead of using the "super" behavior for dissmissing text selection when the user presses outside the selected text.
Hope you can help , thanks ahead.
My WebView with the overriden startActionMode:
#Override
public ActionMode startActionMode(ActionMode.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")) {
webViewDefaultActionModeCallBack = callback;
}
if (mActionMode == null) {
mActionMode = super.startActionMode(mActionModeCallback);
}
return mActionMode;
}
My ActionModeCallback :
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) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.my_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
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.item1:
// do something
break;
case R.id.item2:
// do something
break;
}
mode.finish();
return true;
}
#Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
// Semi-hack in order to clear the selection
// when running Android earlier than KitKat.
if (webViewDefaultActionModeCallBack != null) {
webViewDefaultActionModeCallBack.onDestroyActionMode(mode);
}
}
};

Related

Remove CAB but preserve webview text selection

I want to implement my own custom view (not inflating a menu item), I'm planning to use a toolbar to appear each time contextMenu starts, and hide it when finished.
the problem is: there are only answer showing HOW to clear/inflate another menu over the default actionMode menu
what i`ve tried so far:
-> Use a custom contextual action bar for WebView text selection
Overriding the callback at the WebView
#Override
public ActionMode startActionMode(ActionMode.Callback callback) {
callback2 = new customCallBack();
return super.startActionMode(callback2);
}
public class customCallBack implements ActionMode.Callback {
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
#Override
public void onDestroyActionMode(ActionMode mode) {
}
}
changing the return false to true, doesn't result in the desired behavior i.e. hide the cab
Overriding the OnLongClick is not a option too, since it disable the current selection.
This answer solves the problem:
android webview: prevent text selection actionMode actionBar
not the most elegant solution ever, but I just tested it in an app I'm building and it works like a charm.
The Only way that worked for me (only on on Android L+) is clearing all the menu items from context actionbar from the activity
#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();
}
Toast.makeText(this, "onActionModeStarted", Toast.LENGTH_SHORT).show();
super.onActionModeStarted(mode);
}
#Override
public void onActionModeFinished(ActionMode mode) {
mActionMode = null;
Toast.makeText(this, "onActionModeFinished", Toast.LENGTH_SHORT).show();
super.onActionModeFinished(mode);
}
inspired by Use a custom contextual action bar for WebView text selection
Also I wasn't able to implement the custom menu usin popupWindow or dialogs or dialog fragments.
So simply put it with the webView in a frame layout and play with its visability and margin

How to add more options to Android default Contextual Action Bar

As we know, by default, after selecting some text on views, android displays Contextual Action Bar (CAB) with some default options, such as: copy, cut, select all...
Now, I want to have an application (that has only 2 options: ON/OFF), If I turn it ON, Some other options will be added to default CAB. If I turn it OFF, my custom options will be removed from Android default CAB.
My question is: Is it possible to Add/Remove some options to this default CAB? How can I make above application?
Thank you!
You'll have to use the setCustomSelectionActionModeCallback on each of your TextViews.
You can have a boolean:
boolean on = true;
Then create a method that actually edits the CAB like so:
private void editContextualActionBar(ActionMode actionMode, Menu menu) {
if (on) {
// adds a new menu item to the CAB
// add(int groupId, int itemId, int order, int titleRes)
menu.add(0, R.id.action_to_be_performed, 1, R.string.action_name);
} else {
// removes the new menu item
menu.removeItem(R.id.action_to_be_performed);
}
}
Finally, call the Callback on your TextView with the editContextualActionBar method in onCreateActionMode and perform the menu action in onActionItemClicked:
textView.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
editContextualActionBar(mode, menu);
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_to_be_performed:
// perform action
return true;
default:
break;
}
return false;
}
#Override
public void onDestroyActionMode(ActionMode mode) {
}
});

Use a custom contextual action bar for WebView text selection

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.
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.
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
}
}

Changing options menu/actionbar menu programatically for 3.0+

In short, here's my question:
Can option menus (shown in the actionbar) be modified programatically on android 3.0+?
I have a wizard-style activity in which I use a ViewFlipper to switch between views, or steps.
The steps are: 1 -> 2 -> 3. Only the second screen (2) has a menu item, while the others don't. I have tried hanging on to the Menu reference (source) and either removing/adding items or just hiding/showing them.
#Override
public boolean onCreateOptionsMenu(Menu menu) {
this.mMenu = menu;
getMenuInflater().inflate(R.menu.my_menu, menu);
mMenu.getItem(0).setVisible(false);
return super.onCreateOptionsMenu(menu);
}
Switch to the second screen ->
public void showNext(View v) {
if (mVFlipper.getDisplayedChild() < (mVFlipper.getChildCount() - 1)) {
mVFlipper.showNext();
if (mVFlipper.getDisplayedChild() == 1) {
setTitle("Second screen");
mMenu.getItem(0).setVisible(true);
}
}
}
This works fine on 2.2, but fails miserably on 4.1. Starting off with a visible MenuItem and hiding it later works. Starting off with an invisible menu item and showing it later -
There is a bug in Android's MenuItem setVisible that causes problems when turning items back to visible.
In your onCreateOptionsMenu(), add a check to see if the displayed page needs the Menu, if it does, add the MenuItem. Then, call invalidateOptionsMenu() whenever the page changes. That will rebuild the Menu.
I was also struggling with this issue, then I applied a small hack:
menu1.setEnabled(false);
menu1.setTitle("");
Then where you want to visible it again:
menu1.setEnabled(true);
menu1.setTitle("Okay"); //or you can set text according to your given updated values.
Problem fixed by having the MenuItem be visible after onCreateOptionsMenu finishes and then hiding it from a callback called after onCreateOptionsMenu:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
this.mMenu = menu;
getMenuInflater().inflate(R.menu.my_menu, menu);
boolean dummyVal = super.onCreateOptionsMenu(menu);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
mMenu.getItem(0).setVisible(true);
} else {
mMenu.getItem(0).setVisible(false);
}
return dummyVal;
}
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
if (mMenu != null) {
mMenu.getItem(0).setVisible(false);
}
}
}
If anyone has this problem, I recommend trying toadzky's suggestion first: calling "invalidateOptionsMenu()".

How to recognize whether the Done button is clicked in ActionMode

I use ActionMode to select items in a grid. The problem is that I cannot recognize whether exactly the Done button is clicked. The only I can is to know that ActionMode is finished. But pressing Back finishes the ActionMode too.
The desired behavior is to accept selection on Done click, and exit ActionMode on Back press.
I tried to use ActionMode.setCustomView() but it doesn't affect the Done button. The Activity.onBackPressed() is not called when ActionMode is started.
The one solution I've found is to use ActionBarSherlock and get the Done button manually:
View closeButton = findViewById(R.id.abs__action_mode_close_button);
But it works on Android 2.x-3.x only, because on 4.x a native action bar is used.
Please don't do that as it's implementation specific and extremely non-standard.
You can use the onDestroyActionMode callback for when an action mode is dismissed.
Here is the solution:
ActionMode mMode = MyActivityClass.this.startActionMode(some implementation);
int doneButtonId = Resources.getSystem().getIdentifier("action_mode_close_button", "id", "android");
View doneButton = MyActivityClass.this.findViewById(doneButtonId);
doneButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// do whatever you want
// in android source code it's calling mMode.finish();
}
});
Here is my implementation, and it's a proper hack but it works and I can't really find an alternative to doing something specific when the ActionMode DONE is clicked. I find it really weird that you can't capture this event more elegantly.
Any suggestions to making this slightly less ugly would be greatly appreciated...
In my activity..
boolean mActionModeIsActive = false;
boolean mBackWasPressedInActionMode = false;
#Override
public boolean dispatchKeyEvent(KeyEvent event)
{
mBackWasPressedInActionMode = mActionModeIsActive && event.getKeyCode() == KeyEvent.KEYCODE_BACK;
return super.dispatchKeyEvent(event);
}
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu)
{
mActionModeIsActive = true;
return true;
}
#Override
public void onDestroyActionMode(ActionMode mode)
{
mActionModeIsActive = false;
if (!mBackWasPressedInActionMode)
onActionModeDoneClick();
mBackWasPressedInActionMode = false;
}
public void onActionModeDoneClick();
{
// Do something here.
}
If you are using Fragments with your Activity then some of this code will probably need to be in the Fragment, and the other bits in the Activity.
#JakeWharton (and other ActionBarSherlock users) if you see this on your travels. I'd be interested to know if the above is compatible with ABS as I have yet to integrate ABS with my current project.

Categories

Resources