UIWebView的搜索文本并高亮显示功能

翻译自:search and highlight text in UIWebView. 版权所有, 转载请著明出处,保留链接。

有几个iPhone Apps, 例如如我的”iCab Mobile”, “NewsTap” app, 提供了搜索的特性,即能搜索在UIWebView中显示的文本内容,并且把搜索到的文本内容加黄色背景,以使它们能够很容易的被用户找到。

这篇博客文章将介绍这个特性是怎么实现的。我把这个特性作为UIWebView类的category来实现的,所以你可以很容易在app的UIWebView上应用,使之具有搜索的特性。

首先需要明白的一点是,UIWebView不准许我们直接的访问它的内容,所以我们需要借助javascript。如果你读过我的其他博客文章,你一定已经知道怎么做了。

我们的目标是为新的UIWebView category实现两个新的方法。一个方法是开始搜索并高亮显示搜索的结果,并且返回搜索文本匹配的次数。另一个方法是删除所有加亮过的文本,并且恢复网页原来的布局。

主要代码用javascript来实现,然后用objective-c包装它,来调用javascript代码。

我们从javascript代码着手,正如我在博客文章WebKit on the iPhone所描述的,JavaScript代码将作为资源文件保存在XCode工程中。通过这种方式,在Objective-C 代码中,可以很容易的从应用程序束(application bundle)中加载 javascript代码,避免了不同的编程语言混在同一个文件中(Objective-c和javascript)。

以下代码是javascript实现,稍后我会解释它做了什么工作以及是怎么做的。
SearchWebView.js:

// We're using a global variable to store the number of occurrences
var MyApp_SearchResultCount = 0;

// helper function, recursively searches in elements and their child nodes
function MyApp_HighlightAllOccurencesOfStringForElement(element,keyword) {
      if (element) {
        if (element.nodeType == 3) {        // Text node
              while (true) {
                var value = element.nodeValue;  // Search for keyword in text node
                var idx = value.toLowerCase().indexOf(keyword);

                if (idx < 0) break;             // not found, abort

                var span = document.createElement("span");
                var text = document.createTextNode(value.substr(idx,keyword.length));
                span.appendChild(text);
                span.setAttribute("class","MyAppHighlight");
                span.style.backgroundColor="yellow";
                span.style.color="black";
                text = document.createTextNode(value.substr(idx+keyword.length));
                element.deleteData(idx, value.length - idx);
                var next = element.nextSibling;
                element.parentNode.insertBefore(span, next);
                element.parentNode.insertBefore(text, next);
                element = text;
                MyApp_SearchResultCount++;    // update the counter
              }
        } 
        else if (element.nodeType == 1) { // Element node
              if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') {
                for (var i=element.childNodes.length-1; i>=0; i--) {
                      MyApp_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword);
                }
              }
        }
      }
}

// the main entry point to start the search
function MyApp_HighlightAllOccurencesOfString(keyword) {
      MyApp_RemoveAllHighlights();
      MyApp_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
}

// helper function, recursively removes the highlights in elements and their childs
function MyApp_RemoveAllHighlightsForElement(element) {
      if (element) {
        if (element.nodeType == 1) {
              if (element.getAttribute("class") == "MyAppHighlight") {
                var text = element.removeChild(element.firstChild);
                element.parentNode.insertBefore(text,element);
                element.parentNode.removeChild(element);
                return true;
              } else {
                var normalize = false;
                for (var i=element.childNodes.length-1; i>=0; i--) {
                     if (MyApp_RemoveAllHighlightsForElement(element.childNodes[i])) {
                        normalize = true;
                      }
                   }

                if (normalize) {
                      element.normalize();
                   }
              }
        }

      }
      return false;
}

// the main entry point to remove the highlights
function MyApp_RemoveAllHighlights() {
      MyApp_SearchResultCount = 0;
      MyApp_RemoveAllHighlightsForElement(document.body);
}

搜索文本和移除高亮搜索结果的基本原理是一样的:我们都是基于DOM开发的,这意味着HTML文档是一颗树状结构,HTML元素,文本,注释等,在DOM树形结构中都被表示成一个节点。所有的节点通过父子关系连接在一起。HTML文档的根元素是HTML标志的元素,即<html>....</html>, 该元素通常有连个子元素: HEAD元素和BODY元素。只有BODY元素的内容可见并被显示在屏幕上,所以我们只需要处理文档树中的这部分,即 body部分。

我们需要做的是,从body元素开始,并遍历它的每个子节点,对于每个子节点,也遍历它的每个子节点,直到叶子节点,没有子元素的时候停止。文本节点通常是作为叶子节点存在的,并且文本节点可能包含我们要查找的文本。

遍历文档树搜索全部的文本节点能够通过递归算法(深度遍历算法DFS)实现。DFS算法从树结构的根元素开始(即body元素)到树中的第一个叶子节点(例如根节点的第一个子节点,再到子节点的第一个子节点,这样循环往复,直到到达叶子节点)。然后DFS算法回退到最近的节点(它的子节点没有遍历完),继续访问它的下一个未被访问的子节点。通过这种方式,我们能够遍历树的所有节点,找到所有的文本节点,并在文本节点中搜寻我们要找的文本。

函数MyApp_HighlightAllOccurencesOfStringForElement(element,keyword)MyApp_RemoveAllHighlightsForElement(element)都实现了深度遍历算法(DFS), 他们分别通过MyApp_HighlightAllOccurencesOfString(keyword)MyApp_RemoveAllHighlights()函数调用,在调用前,调用者会做些初始化工作,并把合适的根元素(Body元素)传递给它。开始一个新的搜索的环境初始化是确保当前文档中没有上一个搜索结果的高亮文本,所以我们只需简单的调用函数移除这些高亮文本。

当搜索文本的时候,我们会检查当前的节点是一个文本节点还是一个元素节点。如果是元素节点,它就有子节点,这些子节点也将被遍历。如果是一个文本节点,我们将试法找出该文本节点中的文本是否包含我们要查找的文本。如果有,则插入文本高亮,否则结束对该节点的操作。如果一个节点既不是一个文本节点也不是元素节点,并且没有子节点,那么这个节点不是我们感兴趣的节点,也结束对这个节点的操作。

当一个文本节点的文本包含要搜索的文本,我将把文本节点中的文本分成三块。第一块是匹配文本前的文本,第二块恰好包含匹配要搜索的文本,第三块包含匹配之后的文本。一个新的元素节点(span 元素)将被创建,并作为第二块父节点存在在文档树中。然后,在新创建的SPAN元素上应用CSS样式,使之有高亮的效果(设置背影色为黄色,设置文本颜色为黑色,甚至,你可以增大文本的字体,如果你想的话)。现在连接第一块,SPAN元素,第三块,使之成为文档树的一部分。因为在一个文本节点中可能存在多个匹配的文本,所以继续在第三块中搜索。如果我们在第三块中匹配到了文本,则把第三块再分为三块,否则,则结束对该节点的处理。在创建新元素SPAN的时候,可以给其class属性赋值为"MyAppHighlight"。这点非常重要,因为在使用MyApp_RemoveAllHighlights()删除这些元素的时候,需要找到这些元素,我们将通过class=”MyAppHightlight”标识来找到这些元素。对于删除操作来说,我们也是遍历整棵文档树,不过是寻找元素class属性的值是MyAppHightligth的元素。为了恢复到原来的文档树状态,需要删除插入到文档树中的元素,并且连接分开的文本节点。Javascript能够帮助我们连接文本节点,因为它提供了normalize()函数。

XML DOM normalize() 方法:http://www.w3school.com.cn/xmldom/met_node_normalize.asp
Node 对象参考手册
定义和用法
合并相邻的 Text 节点并删除空的 Text 节点。
语法:
nodeObject.normalize()
说明
这个方法将遍历当前节点的所有子孙节点,通过删除空的 Text 节点,已经合并所有相邻的 Text 节点来规范化文档。该方法在进行节点的插入或删除操作后,对于简化文档树的结构很有用。

在Javascript中,我们能够通过节点的nodeType属性来判断一个节点的类型。值为1则意味着是一个元素节点(如body节点,span节点等)。值为3意味着该节点是一个文本节点,此时,节点的nodeValue属性包含着该文本节点的文本。nodeType的其他值表示当前节点是评论节点(<!- Comment ->), 属性节点(例如a 标签中 HREF属性),文档节点等。在这里,我们只关心元素节点和文本节点。

在上面的实现中,我们通过全局变量记录了搜索文本匹配的次数。

提示:在上面的代码中,可注意到Javascript函数名和变量,以及class 属性,我都使用了比较长的命名,并且有一个”MyApp_前缀”。这样做的原因是,为了避免与现存webpage上的javascript代码中的函数及变量名冲突。如果你使用自己产生的HTML代码并显示在UIWebView对象中,你可以选择更短更简单的命名。但是你处理的HTML和javascript代码来自任何web页面,例如web浏览器,iCab等,你英爱使用长命名,并且以你自己的app名作为变量和函数名的前缀,以避免引起冲突。

Cocoa/Objectic C 部分的实现非常简单。我们只需要声明一个接口,并写一个简单的包装,加载和调用Javascript代码。接口也是简单的,只需要两个方法:一个是开始搜索并标亮匹配的文本,一个是删除高亮标识。

SearchWebView.h:

  @interface UIWebView (SearchWebView)
  - (NSInteger)highlightAllOccurencesOfString:(NSString*)str;
  - (void)removeAllHighlights;
  @end

典型的使用情况是提供一个搜索的文本域,接受用户的输入,输入的文本会传递给highlightAllOccurencesOfString:方法。当用户晃动设备时,app会调用removeAllHighlights:方法移除所有高亮的文本。

实现方法如下:

SearchWebView.m:

@implementation UIWebView (SearchWebView)

- (NSInteger)highlightAllOccurencesOfString:(NSString*)str
{
  NSString *path = [[NSBundle mainBundle] pathForResource:@"SearchWebView" ofType:@"js"];
  NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
  [self stringByEvaluatingJavaScriptFromString:jsCode];

  NSString *startSearch = [NSString stringWithFormat:@"MyApp_HighlightAllOccurencesOfString('%@')",str];
  [self stringByEvaluatingJavaScriptFromString:startSearch];

  NSString *result = [self stringByEvaluatingJavaScriptFromString:@"MyApp_SearchResultCount"];
  return [result integerValue];
}

- (void)removeAllHighlights
{
    [self stringByEvaluatingJavaScriptFromString:@"MyApp_RemoveAllHighlights()"];
}

@end

在方法highlightAllOccurencesOfString:中做的第一件事情是从应用程序束中加载JavaScript文件,并且插入到UIWebView的Web页面中。因为我们是作为UIWebView的一个category来实现的,所以在UIWebView的实例self上能调用stringByEvalutingJavaScriptFromString:

在插入JavaScript代码后,我们只是简单的调用之前定义的Javascript函数来搜索。

最后,我们访问定义在JavaScript代码中的变量,即要搜索文本匹配的次数,我们把它作为方法的返回值。

removeAllHightlights方法中,我们只需要调用相应的JavaScript函数,在这里,不需要加载外部的JavaScript文件并插入web页面中,因为在开始搜索时,我们已经完成了这步,所以现在不需要做了。而如果我们没有开始一个搜索,也不需要插入JavaScript代码,因为没有需要删除的高亮文本。

正如你所预想的,UIWebView category的Objective-c代码只是对Javascript代码的简单封装。如果在你的App中有个UIWebView对象,可以简单的调用highlightAllOccurencesOfString:来查找文本,如下:

// webView is an instance of UIWebView
[webView highlightAllOccurencesOfString:@"keyword"];

注意:如果在你的App的web页面中有frames, 你必须增加些额外的代码,来遍历文档中所有的frames.本章中的示例代码没有这个功能,因为我需要让它尽可能的保持简单。

翻译自:Fsearch and highlight text in UIWebView. 版权所有, 转载请著明出处,保留链接。