如何在安卓中重写Web视图文本选择菜单

21

基本的Android网页文本选择菜单如下图所示。它包括复制、分享、全选和网页搜索等选项。

enter image description here

我希望覆盖这些菜单,并将它们作为我的自定义菜单列表,如“标记颜色”,“标记为重要”等。我查看了stackoverflow上关于上下文菜单的大多数问题。大多数问题都涉及上下文菜单,但结果并不如预期。我想要像下面图片中的菜单。

enter image description here

当我执行选择操作时,Android监视器会显示一些来自viewRoot的视图创建表单。
D/ViewRootImpl: #1 mView = android.widget.PopupWindow$PopupDecorView{648898f V.E...... ......I. 0,0-0,0}
D/ViewRootImpl: #1 mView = android.widget.PopupWindow$PopupDecorView{a66541c V.E...... ......I. 0,0-0,0}
D/ViewRootImpl: MSG_RESIZED_REPORT: ci=Rect(0, 0 - 0, 0) vi=Rect(0, 0 - 0, 0) or=1
D/ViewRootImpl: MSG_RESIZED_REPORT: ci=Rect(0, 0 - 0, 0) vi=Rect(0, 0 - 0, 0) or=1

如何实现这样的实现?

我也阅读了 https://github.com/naoak/WebViewMarker,但没有得到适当的结果。

我已经做了什么?

我扩展了Android的WebView,并希望支持最低SDK 19。 当我执行长按操作时,我会收到长按事件,但我无法获取此类菜单创建API调用。


https://dev59.com/5-o6XIcBkEYKwwoYNBdQ#22563790 - Ashish John
@AshishJohn,我已经阅读了这个答案,但它没有显示第二张图片中的菜单。 - Abhishek
@YvetteColomb 实际上这个问题不需要任何示例,因为它是普通的安卓 Web View 的特性。 - Abhishek
明白了。但我已经添加了我尝试过的内容,例如https://github.com/naoak/WebViewMarker和https://dev59.com/5-o6XIcBkEYKwwoYNBdQ#22563790。所有菜单覆盖问题都显示不同的菜单结构。我会提供一些额外的信息。 - Abhishek
看一下这个:https://dev59.com/TGEh5IYBdhLWcg3wfzpE - AndiM
显示剩余5条评论
4个回答

10

您需要覆盖活动的操作菜单。

更多信息请参阅:https://developer.android.com/guide/topics/ui/menus.html

如何覆盖:

@Override
public void onActionModeStarted(android.view.ActionMode mode) {
    mode.getMenu().clear();
    Menu menus = mode.getMenu();
    mode.getMenuInflater().inflate(R.menu.highlight,menus);
    super.onActionModeStarted(mode);
}

高亮显示

    <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/impclick"
        android:title="Mark As Important"
      />
    <item android:id="@+id/colorclick"
        android:title="Mark with color" />
</menu>

请问您需要在哪个安卓版本上进行此操作?另外,是使用Activity还是Compact-Activity? - Damodhar
你能在这里分享Activity的代码吗?因为它对我来说运行得很好。也许你在Activity层面上做错了什么。 - Damodhar
1
需要更新你的答案...不适用于编译SDK 26及以上版本。 - Abhishek

5

这个解决方案不依赖于Activity的Action模式,适用于所有的Android平台。

我试图回答,但它超出了字符限制,所以我会放一些代码部分

参考链接1:WebView的选择

https://github.com/btate/BTAndroidWebViewSelection

参考链接2:制作WebView标记

https://github.com/liufsd/WebViewMarker

以上两个链接都非常重要,并由一些优秀的开发人员开发。首先需要对来自参考链接1的TextSelectionSupport类进行一些研究。我在这里的选择监听器中自定义了TextSelectionSupport类的两行代码,以获取选择的矩形。

从这里克隆示例项目 https://github.com/ab-cse-2014/WebViewSelection.git

查看CustomWebView的实现和TextSelectionSupport类的使用。

这是我的项目中的WebView类

 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Build;
 import android.support.annotation.RequiresApi;
 import android.support.v7.app.AppCompatActivity;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.webkit.WebView;
 import android.widget.PopupWindow;
 import android.widget.Toast;

 import com.cse.webviewtextselection.R;
 import com.cse.webviewtextselection.webviewmaker.TextSelectionSupport;

 public class CustomWebView extends WebView {

private final String TAG = this.getClass().getSimpleName();

private Context mContext;

private TextSelectionSupport mTextSelectionSupport;

private PopupWindow mPopupWindow;

private int currentTop;

public CustomWebView(Context context) {
    super(context);
    mContext = context;
    initSetUp();
    preparePopupWindow();
}

public CustomWebView(Context context, AttributeSet attrs) {
    super(context, attrs);
    mContext = context;
    initSetUp();
    preparePopupWindow();
}

public CustomWebView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mContext = context;
    initSetUp();
    preparePopupWindow();
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CustomWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    mContext = context;
    initSetUp();
    preparePopupWindow();
}

private void initSetUp() {

    mTextSelectionSupport = TextSelectionSupport.support((AppCompatActivity) mContext, this);
    mTextSelectionSupport.setSelectionListener(new TextSelectionSupport.SelectionListener() {
        @Override
        public void startSelection() {

        }

        @Override
        public void selectionChanged(String text, Rect rect) {
            Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
            showPopAtLocation(mPopupWindow, rect.left, rect.top);
        }

        @Override
        public void endSelection() {
            if (mPopupWindow != null) {
                mPopupWindow.dismiss();
            }
        }
    });
}

private void preparePopupWindow() {

    LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View customPopupView =  layoutInflater.inflate(R.layout.custom_popup_layout, null);
    mPopupWindow = new PopupWindow(customPopupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true);
    mPopupWindow.setAnimationStyle(android.R.style.Animation_Dialog);

}

private void showPopAtLocation(PopupWindow mPopupWindow, int x, int y) {

    if (mPopupWindow != null) {

        if (currentTop != 0 || currentTop > ((AppCompatActivity)mContext).getWindow().getDecorView().getHeight()) {

                if (y > currentTop) {

                    y -= currentTop;

                }

        }

        Log.d("Current Top : ", String.valueOf(currentTop));
        Log.d("Y : ", String.valueOf(y));

        //mPopupWindow.showAtLocation(((AppCompatActivity)mContext).findViewById(R.id.parentRelativeLayout), Gravity.NO_GRAVITY, x, y);
        mPopupWindow.showAtLocation(((AppCompatActivity)mContext).getWindow().getDecorView(), Gravity.NO_GRAVITY, x, y);


    }

}

@Override
protected void onScrollChanged(int newLeft, int newTop, int oldLeft, int oldTop) {

    currentTop = newTop;


    super.onScrollChanged(newLeft, newTop, oldLeft, oldTop);
}
 }

自定义弹出式菜单的XML(例如Android的智能文本选择(custom_popup_layout.xml))。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/myCustomMenuLinearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@android:color/transparent">

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="@android:color/white"
    android:elevation="5dp"
    android:layout_margin="12dp">

    <TextView
        android:id="@+id/menuOne"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Mark With Color"
        android:textColor="@android:color/black"
        android:padding="10dp"
        android:maxLines="1"/>

    <TextView
        android:id="@+id/menuTwo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Mark As Important"
        android:textColor="@android:color/black"
        android:padding="10dp"
        android:maxLines="1"/>

    <TextView
        android:id="@+id/menuThree"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Show More"
        android:textColor="@android:color/black"
        android:padding="10dp"
        android:maxLines="1"/>

</LinearLayout>



</LinearLayout>

输出屏幕截图

选择一

选择二

选择三


当我们使用“loadDataWithBaseURL”时,这个解决方案不起作用。 - Yerram Naveen
我在上面的回答中没有使用"loadDataWithBaseURL"。你想要实现什么? - Abhishek
我使用了上述代码,当我使用webView.loadUrl("file:///android_asset/XXX.html")时,它可以正常工作。但是,当我使用webview.loadDataWithBaseUrl(baseUrl, data,mimeType,encoding,historyUrl)时,在控制台中会出现错误信息"Uncaught ReferenceError: android is not defined"。 - Yerram Naveen
@YerramNaveen - 证明了在webview.loadDataWithBaseUrl(baseUrl, data,mimeType,encoding,historyUrl)中用作参数的值。 - Abhishek
1
我解决了问题,我们需要在HTML头部添加以下脚本:<script src='jquery-1.8.3.js'></script> <script src='jpntext.js'></script> <script src='rangy-core.js'></script> <script src='rangy-serializer.js'></script> <script src='android.selection.js'></script> 非常感谢。 - Yerram Naveen
显示剩余9条评论

4

你需要的是动作模式,在活动中:

    @Override
    public void onActionModeStarted(ActionMode mode) {
        Menu menu = mode.getMenu();

        // you can remove original menu: copy, cut, select all, share ... or not
        menu.clear();

        // here i will get text selection by user
        menu.add(R.string.action_menu_preview_card)
                .setEnabled(true)
                .setVisible(true)
                .setOnMenuItemClickListener(item -> {
                    if (mWebview != null) {
                        mWebview.evaluateJavascript("window.getSelection().toString()", value -> {
                            value = StringUtil.trimToNull(value);
                            if (value != null) {
                                // do something about user select
                            }
                        });
                    }
                    // Post a delayed runnable to avoid a race condition
                    // between evaluateScript() result and mode.finish()
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mode.finish();
                        }
                    }, 200);
                    return true;
                });
        super.onActionModeStarted(mode);
    }

我已经在Android版本21及以上测试过了,这可以处理Action Mode菜单的点击,但mode.getMenuInflater().inflate(...)无法做到这一点。

1
能够显示菜单,但 window.getSelection().toString() 返回空字符串。 - Yerram Naveen
1
它的工作很好,除了mode.finish()和evaluateJavascript()之间存在竞争条件。发布延迟的Runnable并在那里调用mode.finish()。我会尝试编辑代码示例以反映这一点。 - gregko

1
我认为这里可以帮助你。
为了完整起见,以下是我解决问题的方法:
我按照这个答案的建议进行了操作,并进行了一些微调以更接近被覆盖的代码: 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 { ... // 到这一点为止和问题中的内容相同

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

}

这就是全部内容。请注意,只要您使用了覆盖的startActionMode的MyWebView,就没有办法获得本机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);
}

}

这是一个示例菜单,可供您开始使用:

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

这就是全部!你完成了!现在你的自定义菜单将会出现,你不必担心选择和ActionMode生命周期,只需略微关注即可。
如果使用占据整个父Activity的WebView,这几乎可以完美工作。但我不确定如果同时在Activity中有多个视图,它是否能够正常工作。在这种情况下,可能需要进行一些微调。

需要更新。不支持编译SDK 26及以上版本。 - Abhishek

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