为WebView文本选择使用自定义上下文操作栏

27

我使用了来自谷歌的这个指南这篇教程来创建我的自定义上下文操作栏。

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

这个菜单是为了在用户选择文本时出现,所以它会覆盖原生的复制/粘贴菜单。现在我要说我的问题。

因为我正在覆盖文本选择功能,我还向WebView添加了一个LongClickListener并实现了onLongClick(View v)方法,以便我可以检测到用户进行选择的操作。

    myWebView.setOnLongClickListener(new View.OnLongClickListener() {

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

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

当我长按时,我看到我的自定义菜单出现了,但没有文本被突出显示。
我需要有文本选择功能;没有它,我的菜单就毫无意义了。

我如何覆盖 onLongClick(View v),但保持 Android 提供的文本选择功能?
如果不可能,我能否在其他地方调用 startActionMode(mActionModeCallback),以便文本会像正常情况下一样被选中,但我的自定义菜单也会出现?
如果这两个都不可能......请帮帮我。

2个回答

42

有更简单的方法!请看下面的更新:D


为了完整起见,这是我如何解决问题的:

我遵循了这个答案的建议,并进行了一些微调,以更好地匹配被覆盖的代码:

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);
    }
}

本质上,这迫使您的自定义CAB出现而不是Android CAB。现在,您需要修改回调函数,以便文本突出显示随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;
        }
    }
}

这就是全部内容。请注意,只要您使用具有覆盖的startActionModeMyWebView,就无法获取本地CAB(在WebView中的拷贝/粘贴菜单)。可能可以实现这种行为,但这不是此代码的工作方式。


更新:有一种更简单的方法可以做到这一点!上面的解决方案很好用,但这是一种替代的、更简单的方法。

这种解决方案对ActionMode的控制较少,但所需的代码比上述解决方案要少得多。

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);
    }
}

以下是一个菜单示例,帮助您入门:

<!-- 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>

完成了!现在您的自定义菜单将显示出来,您不必担心选择,也几乎不需要关注ActionMode生命周期。

如果使用占据整个父ActivityWebView,这将几乎完美地运行。如果同时有多个View在您的Activity中,则可能需要进行一些微调。


我做了类似于你的解决方案,但我的onDestroyActionMode没有被调用。你遇到过这样的问题吗? - androidevil
谢谢。我的项目是为4.0版本设计的。 - androidevil
1
很好的发现。我已经进行了相应的编辑。将来,你可以更主动地自己编辑,而不是评论。在问题中编辑显著的错别字,比如这个问题,只是成为一个优秀的SO社区成员的一部分。 :) - Sean Beach
这行代码 android:onClick="onContextualMenuItemClicked" 在启动菜单时导致应用程序崩溃。 - coder
1
如果您选择更多文本(即滚动选择文本光标),则在Android M版本以上,所有默认菜单将再次出现,这将无法正常工作。 - Brajendra Pandey
显示剩余17条评论

1
我曾经做类似的事情的方法是仅覆盖onTouchListener并调用GestureDetector来检测WebView何时被长按,然后从那里执行我想要的操作。以下是一些示例代码,允许您在不损失WebView文本选择的情况下捕获长按事件。希望这能有所帮助。
@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
    }

}

你的建议并没有错,它确实可以为长按操作提供额外的功能。然而,这并不能达到我想要做的事情。即使我在onLongPress调用中加入了startActionMode(mActionModeCallback),原生的上下文菜单仍然会出现。我想要覆盖掉该菜单,同时保留文本选择功能。 - Sean Beach
啊,这样更清楚了。我以为我可能有点误解你的问题。这个问题似乎与你的问题非常相似,但还没有被接受的解决方案。https://dev59.com/5-o6XIcBkEYKwwoYNBdQ - anthonycr
但是这个问题表明有一种方法可以覆盖复制和粘贴时显示的内容。https://dev59.com/gGUp5IYBdhLWcg3wS2PP - anthonycr
我编辑了我的答案,并加入了一些可能有效的内容。如果检测到长按,则尝试从onTouch返回true。我以前没有使用过这个,所以我不确定它是否能像你想要的那样工作。 - anthonycr
我按照你发布的第二个链接中提供的建议进行操作,文本选择现在可以正常工作。但是,我现在遇到了另一个问题。(我将发布一个新的问题来解决它。) - Sean Beach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接