WebView实现改变选中区域颜色以及添加下划线

一、Web端实现

最近遇到了一个问题,老大要求实现对WebView加载的html页面实现选中修改背景色以及添加下划线并可以删除,最后可以保存这些修改,下次进入该html界面时仍然能够显示之前添加的背景色以及下划线。

1.首先需要解决的问题是如何实现对选中的html页面区域添加背景色以及添加下划线。

因为考虑到要保存的问题,这部分实现应该在前端实现,通过查找资料,一开始想到的是使用surroundContents来实现。如下代码:

       function setColor(){
            var tr = window.getSelection().getRangeAt(0);
            var span = document.createElement("span");
            span.style.cssText = "color:#ff0000";
            tr.surroundContents(span);
        }

上述代码确实能够实现给选中区域添加背景色,但它有一个不足就是无法实现跨标签实现选中添加背景色。即选中单个<div></div>中内容时可以实现效果,但是如果连续选中两个div时,这种方法就不起作用了。并且通过console控制台发现如下报错:

所以这个方案pass掉。

 

很快,又找到了另一套方案,使用range.insertNode实现,代码如下:

  function setColor() {
            var range = window.getSelection().getRangeAt(0), 
            span = document.createElement('span');
            span.className = 'highlight';
            span.style.cssText = "color:#ff0000";
            span.appendChild(range.extractContents());
            range.insertNode(span);
        }

但是上述代码也确实能够实现给选中区域添加背景色,并且不存在跨标签的问题,但是它也有一个问题:就是会破坏原有的标签结构。举个栗子:

比如原本的标签结构是:

 

原本的界面如下:

执行第二种方案的js方法后,如果是跨标签选中,标签结构改变为:

 

界面改变为:

通过对比发现第二套方案的问题是其改变了原本的标签结构,导致界面变样。

 

终于找到了第三种实现方案,使用mark.js实现,这个库真的很厉害,实现这个库实现选中区域添加背景色以及添加下划线,可以避免上述两种方案的不足。

代码如下:

    <script type="text/javascript">

        function callInterface(spanId){
            JSCallBackInterface.callback(spanId);
        }

        function removeSpan(spanId) {
            $(spanId).unmark();
        }

        function setColor() {
            highlight("", "red", 1);
        }

        function setUnderLine(){
            highlight("", "red", 2);
        }

        var selectionRange;
        function highlight(highlightID, color, type) {
            console.log("highlight:" + color);
            if (window.getSelection && window.getSelection().toString()) {
                var node = getSelectionParentElement();
                if (node != null) {
                    var text = getSelectionText();
                    console.log("Selected text: " + text);
                    if (type === 1) {
                        markFunc(node, text, /*HIGHLIGHT_CLASS + " " + */color);
                    } else {
                        markFuncUnderLine(node,text);
                    }
                } else {
                    console.log("Parent nde is null for some reason");
                }
            } else {
                console.log("tapped without selection");
            }
        }

        function getSelectionText() {
            if (window.getSelection) {
                var sel = window.getSelection();
                return sel.toString();
            }
        };

        function getSelectionParentElement() {
            var parentEl = null,
                sel;
            if (window.getSelection) {
                sel = window.getSelection();
                if (sel.rangeCount) {
                    selectionRange = sel.getRangeAt(0);
                    parentEl = selectionRange.commonAncestorContainer;
                    if (parentEl.nodeType != 1) {
                        parentEl = parentEl.parentNode;
                    }
                }
            } else if ((sel = document.selection) && sel.type != "Control") {
                parentEl = sel.createRange().parentElement();
            }
            return parentEl;
        };

        function markFunc(node, text, color) {
            var instance = new Mark(node);
            instance.mark(text, {
                "element": "span",
                "className": color,
                "acrossElements": true,
                "separateWordSearch": false,
                "accuracy": "partially",
                "diacritics": true,
                "ignoreJoiners": true,
                "each": function(element) {
                    element.setAttribute("id", "span_" + (new Date().getTime()));
                    element.setAttribute("title", "sohayb_title");
                    element.setAttribute("onclick", "callInterface($(this).attr('id'))");
                    element.setAttribute("onmouseover", "callInterface($(this).attr('id'))");
                },
                "done":function(totalMarks) {
                    window.getSelection().empty();
                    console.log("total marks: " + totalMarks);
                },
                "filter": function(node, term, totalCounter, counter) {
                    var res = false;
                    if (counter == 0) {
                        res = selectionRange.isPointInRange(node, selectionRange.startOffset);
                    } else {
                        res = selectionRange.isPointInRange(node, 1);
                    }
                    console.log("Counter: " + counter + ", startOffset: " + selectionRange.startOffset);
                    return res;
                }
            });
        };

        function markFuncUnderLine(node, text) {
            var instance = new Mark(node);
            instance.mark(text, {
                "element": "span",
                "acrossElements": true,
                "separateWordSearch": false,
                "accuracy": "partially",
                "diacritics": true,
                "ignoreJoiners": true,
                "each": function(element) {
                    element.setAttribute("id", "span_" + (new Date().getTime()));
                    element.setAttribute("title", "sohayb_title");
                    element.setAttribute("style","text-decoration:underline blue");
                    element.setAttribute("onclick", "callInterface($(this).attr('id'))");
 element.setAttribute("onmouseover", "callInterface($(this).attr('id'))");
                },
                "done":function(totalMarks) {
                    window.getSelection().empty();
                    console.log("total marks: " + totalMarks);
                },
                "filter": function(node, term, totalCounter, counter) {
                    var res = false;
                    if (counter == 0) {
                        res = selectionRange.isPointInRange(node, selectionRange.startOffset);
                    } else {
                        res = selectionRange.isPointInRange(node, 1);
                    }
                    console.log("Counter: " + counter + ", startOffset: " + selectionRange.startOffset);
                    return res;
                }
            });
        };
    </script>

下面讲述一下上述js方法的作用:

function callInterface(spanId){
    JSCallBackInterface.callback(spanId);
}

callInterface的作用是将需要删除的高亮,或者下划线的id告知客户端。该方法在用户点击添加了高亮或者下划线的<span></span>块时调用,代码如下:

 element.setAttribute("onclick", "callInterface($(this).attr('id'))");
 element.setAttribute("onmouseover", "callInterface($(this).attr('id'))");

客户端拿到spanId后可调用以下方法删除高亮后者下划线:

function removeSpan(spanId) {
    $(spanId).unmark();
}

实现高亮和下划线的方法是:setColor和setUnderLine方法,它们都调用了highlight方法,参数type为1表示调用mark.js实现高亮,参数type为2表示调用make.js实现添加下划线。

function setColor() {
    highlight("", "red", 1);
}

function setUnderLine(){
    highlight("", "red", 2);
 }

实现高亮和下划线的思路是:

获取选中内容,以及内容的首个父元素节点,交给mark.js处理。高亮通过给mark方法中className属性设置颜色指定,下划线则通过给元素添加element.setAttribute("style","text-decoration:underline blue")属性指定。

 

 

简单介绍一下mark.js:

mark.js是用JavaScript编写的实现文本高亮的方案。 它可用于动态标记搜索词或自定义正则表达式,并为您提供内置选项,如变音符号支持,单独的单词搜索,自定义同义词,iframes支持,自定义过滤器,精度定义,自定义元素,自定义类名等。

一些基本API的介绍:

mark()方法:显示高亮的方法。

例子:

JavaScript:

var context = document.querySelector(".context");
var instance = new Mark(context);
instance.mark(keyword [, options]);

注意:即使这是一个链式方法,因此允许您在返回对象上调用更多方法,建议始终使用完成回调,因为mark.js可以异步工作。

 

参数 keywords:字符串或字符串数组。 要标记的关键字。 也可以是包含多个关键字的数组。 请注意,关键字将被转义。 如果您需要标记未转义的关键字(例如包含模式),请查看下面的markRegExp()方法。


options 类型:对象 
可选选项:

element  用于包装匹配的HTML元素,例如 span
className  将附加到元素的类名
separateWordSearch 是否搜索由空格分隔的每个单词而不是完整的单词
acrossElements 是否跨元素搜索匹配项
diacritics  如果应该匹配变音字符。 例如“piękny”也匹配“piekny”,“doner”也匹配“döner”
ignoreJoiners  是否还找到包含软连字符,零宽度空间,零宽度非连接器和零宽度连接器的匹配项。 它们用于指示换行点,其中没有足够的空间来显示完整的单词
each 每个标记元素的回调。 接收标记的DOM元素作为参数
done 完成所有标记后的回调函数。 接收标记总数作为参数
filter function一个回调来过滤或限制匹配。 
将为每个匹配调用它并接收以下参数:
包含匹配的文本节点,
已找到的匹配数,
总的标记数,
对匹配的标记数,
如果标记应该停止则该函数必须返回false,否则为true。

 

二、android客户端实现

客户端的要求是长按选中WebView中的文字可以选择高亮显示、或者选择添加下划线,对于已经添加了高亮或者下划线的文字,可以删除高亮、或者下划线。

1.要实现长按,弹出可供用户选择的弹框,并且弹框中要保留系统原有的功能(复制、分析、全选、网页搜索)。

所以我采用ActionMode实现,通过自定义WebView,重写其startActionMode方法,来实现想要的效果。

代码如下:


@Override
public ActionMode startActionMode(ActionMode.Callback callback) {
    ActionMode actionMode = super.startActionMode(callback);
    return resolveActionMode(actionMode);
}

@Override
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
    ActionMode actionMode = super.startActionMode(callback, type);
    return resolveActionMode(actionMode);
}
    
/**
 * 处理item,处理点击
 * @param actionMode
 */
private ActionMode resolveActionMode(ActionMode actionMode) {
    if (actionMode != null) {
        final Menu menu = actionMode.getMenu();
        mActionMode = actionMode;
      //  menu.clear();
        for (int i = 0; i < mActionList.size(); i++) {
            menu.add(mActionList.get(i));
        }
        for (int i = 0; i < menu.size(); i++) {
            MenuItem menuItem = menu.getItem(i);
            menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
                @Override
                public boolean onMenuItemClick(MenuItem item) {
                    getSelectedData((String) item.getTitle());
                    releaseAction();
                    return true;
                }
            });
        }
    }
    mActionMode = actionMode;
    return actionMode;
}


上述代码中的集合mActionList 用于存储自定义的按钮,比如(添加高亮、添加下划线、删除)。

然后要对每个自定义的menu添加点击事件,如上述代码中的menuItem.setOnMenuItemClickListener中重写onMenuItemClick方法,具体代码如下:

private void getSelectedData(String title) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        if (getResources().getString(R.string.add_highlight).equals(title)) {//高亮
            evaluateJavascript("javascript:setColor()", null);
        } else if (getResources().getString(R.string.add_underline).equals(title)) {//添加下划线
            evaluateJavascript("javascript:setUnderLine()", null);
        } else if (getResources().getString(R.string.remove_span).equals(title)) {//删除高亮或者下划线
            evaluateJavascript("javascript:removeSpan("+ mRemoveSpanId +")", null);
            mRemoveSpanId = null;
        }
    } else {
        if (getResources().getString(R.string.add_highlight).equals(title)) {//高亮
            loadUrl("javascript:setColor()");
        } else if (getResources().getString(R.string.add_underline).equals(title)) {//添加下划线
            loadUrl("javascript:setUnderLine()");
        } else if (getResources().getString(R.string.remove_span).equals(title)) {//删除高亮或者下划线
            loadUrl("javascript:removeSpan("+ mRemoveSpanId +")");
            mRemoveSpanId = null;
        }
    }
}

在实现高亮以及添加下划线、删除的功能都是调用js实现,注意在删除的时候需要上传参数mRemoveSpanId,来告知要删除哪一个高亮或者下划线。

客户端获取mRemoveSpanId的方式是通过Js调用android代码,客户端记录mRemoveSpanId,代码如下:

private class JsCallAndroidInterface {

    @JavascriptInterface
    public void callback(String spanId) {
        mRemoveSpanId = spanId;
    }

    @JavascriptInterface
    public void showSource(String html) {
        mContent = html;
    }
}

public void removeSpanJSInterface() {
    addJavascriptInterface(new JsCallAndroidInterface(), "JSCallBackInterface");
}

下面是使用WebView的基本设置:

private void configWebView() {
    WebSettings webSettings = mWebView.getSettings();
    webSettings.setAllowContentAccess(true);
    webSettings.setAllowFileAccess(true);
    webSettings.setAllowFileAccessFromFileURLs(true);
    webSettings.setAllowUniversalAccessFromFileURLs(true);
    webSettings.setAppCacheEnabled(true);
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
    webSettings.setDatabaseEnabled(true);
    webSettings.setDomStorageEnabled(true);
    webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);
    webSettings.setSupportZoom(false);
    webSettings.setBuiltInZoomControls(false);
    webSettings.setDisplayZoomControls(false);
    webSettings.setPluginState(WebSettings.PluginState.ON); //player net video
    webSettings.setLoadsImagesAutomatically(true);
    webSettings.setLoadWithOverviewMode(true);
    webSettings.setTextZoom(100);
    webSettings.setUseWideViewPort(true);
    webSettings.setJavaScriptEnabled(true);
    webSettings.setDomStorageEnabled(true);
    webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
    webSettings.setGeolocationEnabled(true);
    webSettings.setSupportMultipleWindows(false);
    webSettings.setSaveFormData(true);
    mWebView.setActionList(mItems);//添加自定义Item
    mWebView.removeSpanJSInterface();//添加javascriptInterface 
}


/** 
  * 添加javascriptInterface 
  * 第一个参数:这里需要一个与js映射的java对象 
  * 第二个参数:该java对象被映射为js对象后在js里面的对象名,在js中要调用该对象的方法就是通过这个来调用 
  */  
public void removeSpanJSInterface() {
    addJavascriptInterface(new JsCallAndroidInterface(), "JSCallBackInterface");
}

最后千万别忘了添加网络权限: <uses-permission android:name="android.permission.INTERNET" />

 

还有一些注意点:

1.比如:webview 网络页面从本地(assets)加载js库的方式:

客户端:

 

String local = "file:///android_asset";
mWebView.loadDataWithBaseURL(local, content, "text/html", "UTF-8", null);

Web端:

    <link rel="stylesheet" href="file:///android_asset/zEditor.css">
    <script type="text/javascript" src="file:///android_asset/jquery.min.js"></script>
    <script type="text/javascript" src="file:///android_asset/jquery.mark.js"></script>

2.注意WebView的内存泄漏问题

@Override
protected void onDestroy() {
    if (mWebView != null) {
        mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        ((ViewGroup) mWebView.getParent()).removeView(mWebView);
        mWebView.stopLoading();
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.clearView();
        mWebView.removeAllViews();
        mWebView = null;
    }
    super.onDestroy();
}

 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值