一、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() {@Overridepublic 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 {@JavascriptInterfacepublic void callback(String spanId) {mRemoveSpanId = spanId;}@JavascriptInterfacepublic 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 videowebSettings.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);//添加自定义ItemmWebView.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();
}

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

  1. 【ps】如何使用photo shop改变指定区域颜色

    打开photoshop界面,图像文件拖动到photoshop界面打开.建议先创建一个新的[图层]. 使用鼠标选中先前的图层, 点击上方功能选项的**[图像]-[调整]-[替换颜色]**选项. 然后在对 ...

  2. 使用NSMutableAttributedString添加下划线、删除线、阴影、填充、不同字体颜色等

    在iOS开发中,有时会遇到需要添加下划线,或者设置字符串中某几个字的颜色的情况,最常见的栗子就是注册页面,如图所示: 几乎所有注册页面中都会出现这么一句话 "点击下一步表示您已同意<用 ...

  3. delphi edit里面的文字如何添加下划线_标题设计如何处理更吸引人?来看设计高手的实用技巧...

    计是在向用户传递信息,在设计的日常工作中,传递信息的方式大多都是图文相结合的形式,而文字作为信息传递中最直观的表现形式,对于整体而言是至关重要的一环.很多设计师一味强调追求图具有吸引力,而忽略了文字的 ...

  4. 如何给PDF中的内容添加下划线

    现在我们使用的电子文档大多都是PDF格式,在阅读的过程中对于重点的部分需要添加下划线进行标注,要怎么操作呢?下面我们一起看看PDF文档添加下划线的方法. 方法1:注释 首先用极速PDF阅读器打开文件后 ...

  5. 赛效:WPS如何给文档内容添加下划线

    在文档中,给文本内容加下划线是非常有必要的.下划线用于强调内容,比如在标题.重要段落或关键词中加上下划线,以强调文本中的重要部分.下划线的样式和颜色也是可以设置的,从某种程度上来说,还起到了美化文档的 ...

  6. 安卓PDF阅读器使用技巧Ⅱ:PDF文档添加批注、添加下划线及删除技巧

    如何更好地使你的PDF文档便于使用?以确保你在后续的阅读/编辑中可以更加得心应手?如果手头只有一部手机的情况下,我们可以借助PDF阅读器的的编辑功能,通过高亮显示.增加下划线等操作,来完成你对文档的重 ...

  7. html5添加下划线虚线,web中添加下划线的方法及优缺点

    有很多种添加下划线样式的方法.可能你还记得<Crafting link underlines on Medium>这篇文章.Medium 并没有尝试特殊的方法,只是想创建一个漂亮的看起来正 ...

  8. 极速office(Word)怎么在空白处添加下划线

    我们在制作文件时,有时需要在文字的后面添加下划线,那么怎么添加呢?以最常用的极速办公极速office为列 首先,下划线末尾处添加一个符号,如图: 然后,用光标选中需要添加下划线的空白处,如图: 接着, ...

  9. TextView显示html信息、在文本下面添加下划线、中划线、设置图片

    1,在文本下面添加下划线 tv.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG); 2,在文本设置中划线并加清晰 tv.getPaint().setFlag ...

最新文章

  1. Larbin简介,及其在Ubuntu10.04下的编译安装
  2. [iPhone高级] 基于XMPP的IOS聊天客户端程序(IOS端三)
  3. python之路——网络编程
  4. 模式对话框与非模式对话框的区别2
  5. python socket模块 和pyqt_使用PyQt和Socket进行聊天编程[标准库]
  6. FTP服务器软件 虚拟目录,FTP服务器软件 虚拟目录
  7. 【笔记】生成函数与大背包问题
  8. 文荣:7月24日阿里云上海峰会网络大神
  9. 定点补码加减法运算_定点整数的加减法
  10. 通过监听手势滑动解决DrawerLayout只能边缘打开抽屉问题
  11. Java 计算排列_java如何进行排列组合运算
  12. 高数下-空间几何(一)-向量
  13. 巧妙设置win7系统给WPS文档加密
  14. 基于BERT做中文文本分类(情感分析)
  15. 数据挖掘之人工神经网络
  16. php 数组的结构和定义
  17. mkdocs 部署教程
  18. C/C++趣味代码-------狸猫换太子
  19. 览沃livox_大疆内部孵化的览沃科技Livox推出激光雷达,进入自动驾驶领域
  20. 使用pymysql连接数据库

热门文章

  1. 声纹识别之I-Vector
  2. Redis——事务 锁机制
  3. 群晖NAS 7.X版搭建博客网站,并内网穿透发布公网可访问 4-8
  4. 常见多变量/多元统计分析方法分类图
  5. 消防设施操作员考试真题、模拟练习题库(6)
  6. 浅析SIEM、态势感知平台、安全运营中心
  7. android锁定屏幕通知_如何在Android锁定屏幕上隐藏敏感通知
  8. 信息传递(带权并查集求最小环
  9. ★ 我的世界各类奇葩武器实现!(命令方块1.13+)
  10. 【更新7】ARCH和GARCH模型