定制UIWebView的上下文菜单

翻译自:customize the contextual menu of UIWebView. 版权所有, 转载请著明出处,保留链接。

当你点击一下或把手指放在UIWebView对象的一个链接上一秒,一个上下文菜单将会打开并提供了几个选项:复制URL到剪切板,打开链接,一个关闭菜单的按钮。不幸的是没有可以向这个上下文菜单添加额外的菜单选项的API。但是如果你看下iCab Mobile, 你会发现有好多的额外菜单项,这是怎么做到的呢?

首先需要明确的是,你真的不能在默认的标准上下文菜单中增加额外的菜单项。不过,你可以关闭上下文菜单,而使用CSS样式。所以解决方法是关闭默认的菜单,然后重头实现自己的菜单。在实现自定义的菜单前,你必须先捕获tap-and-hold手势,获取手指在屏幕上的坐标,并把它转换成web页面上的坐标,然后查找该坐标处的HTML元素。基于HTML元素,你能确定在上下文菜单中包含哪些菜单项,然后创建并显示上下文菜单。

第一步是在UIWebView中关闭默认的上下文菜单。这很简单,因为只需把web页面的body元素的CSS属性“-webkit-touch-callout”的值设为”none”。我们通过在UIWebView的委托方法“webViewDidFinishLoad:”中调用JavaScript代码来完成:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
       [webView stringByEvaluatingJavaScriptFromString:@"document.body.style.webkitTouchCallout='none';"];
}

现在,默认的上下文菜单将不会出现。

第二步,捕获tap-and-hold手势。如果你的App只需运行在iOS3.2, iOS4.0或者更新的iOS版本上,只需简单的使用UIGestureRecognizer API。但是你任然想要兼容第一代的iPod Touch 和 iPhone设备,你要做更多些工作,因为这些设备上不支持 UIGestureRecognizer。在这里,我会介绍兼容所有设备的方法。

一个比较头痛的问题,在UIWebView中捕获手势,是UIWebView内部有很多嵌套的views组成,但只有内部view才对手势和事件反应。这意味着你在UIWebView的子类中重写”touchesBegan:withEvent:”,”touchesMoved:withEvent:”, 这些方法不会被调用。事件直接投递给这些私有的views, 然后这些views完成工作。如果你重写”hitTest:withEvent:”, 来找到哪个视图投递事件,这样你能获取触摸的事件,但是你也必须自己处理把事件投递到相应的内部视图中。因为内部视图是私有的,并且它们间的关系是不透明的,所以这样做极其的危险。

一条更容易的方法是子类化UIWindow并重写sendEvent方法,这里你能够获取所有的事件,在它们投递给视图之前。我们能通过notification管理器来投递tap-and-hold事件,每一个对这个手势感兴趣的对象可以监听这个notification。

识别tap-and-hold手势不是非常的复杂。我么需要做的是保存手指在屏幕上的坐标当手指第一次触碰到屏幕时。此时,我们启动一个1秒的定时器。只要另一个手指触碰到屏幕,或者手指移动,触摸事件就被取消,并使定时器失效,因为这已不再是一个简单的tap-and-hold手势了。如果定时器到期,就能确定一个单独的手指在屏幕上触摸了一秒钟,并且没有移动,那么我们就能识别为tap-and-hold手势,然后我们把这个手势作为一个通知提交。

如下是UIWindow子类的实现:

MyWindow.h:

@interface MyWindow : UIWindow
{
       CGPoint    tapLocation;
       NSTimer    *contextualMenuTimer;
}
@end

MyWindow.m:

#import "MyWindow.h"

@implementation MyWindow

- (void)tapAndHoldAction:(NSTimer*)timer
{
       contextualMenuTimer = nil;
       NSDictionary *coord = [NSDictionary dictionaryWithObjectsAndKeys:
         [NSNumber numberWithFloat:tapLocation.x],@"x",
         [NSNumber numberWithFloat:tapLocation.y],@"y",nil];
       [[NSNotificationCenter defaultCenter] postNotificationName:@"TapAndHoldNotification" object:coord];
}

- (void)sendEvent:(UIEvent *)event
{
       NSSet *touches = [event touchesForWindow:self];
       [touches retain];

       [super sendEvent:event];    // Call super to make sure the event is processed as usual

       if ([touches count] == 1) { // We're only interested in one-finger events
          UITouch *touch = [touches anyObject];

          switch ([touch phase]) {
             case UITouchPhaseBegan:  // A finger touched the screen
                   tapLocation = [touch locationInView:self];
                [contextualMenuTimer invalidate];
                contextualMenuTimer = [NSTimer scheduledTimerWithTimeInterval:0.8
                    target:self selector:@selector(tapAndHoldAction:)
                    userInfo:nil repeats:NO];
            break;

             case UITouchPhaseEnded:
             case UITouchPhaseMoved:
             case UITouchPhaseCancelled:
                [contextualMenuTimer invalidate];
                contextualMenuTimer = nil;
                break;
          }
       } else {                    // Multiple fingers are touching the screen
          [contextualMenuTimer invalidate];
          contextualMenuTimer = nil;
       }
       [touches release];
}

@end

UITouchPhaseMoved阶段,对于准许在屏幕小的移动将非常有用。你可以增加些代码检查手指移动的距离,如果在准许的范围内,你可以不用终止定时器。这将会帮助那些手指保持触摸一秒钟有困难的用户。

另一件重要的事情是,当App 的window通过NIB文件创建时,必须在Interface Builder NIB 文件中把window的 UIWindow类 改变成 新的UIWindow的子类。通过这种方式,窗口将会通过我们的子类创建。

第三步是在UIWebView委托中监听”TapAndHoldNotification”通知,当收到通知时,需要检查哪个HTML元素被触碰了。

当初始化UIWebView委托时,需要把委托作为观察者增加到通知中心中:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextualMenuAction:) name:@"TapAndHoldNotification" object:nil];

以下是”contextualMenuAction:”的实现:

- (void)contextualMenuAction:(NSNotification*)notification
{
       CGPoint pt;
       NSDictionary *coord = [notification object];
       pt.x = [[coord objectForKey:@"x"] floatValue];
       pt.y = [[coord objectForKey:@"y"] floatValue];

       // convert point from window to view coordinate system
       pt = [webView convertPoint:pt fromView:nil];

       // convert point from view to HTML coordinate system
       CGPoint offset  = [webView scrollOffset];
       CGSize viewSize = [webView frame].size;
       CGSize windowSize = [webView windowSize];

       CGFloat f = windowSize.width / viewSize.width;
       pt.x = pt.x * f + offset.x;
       pt.y = pt.y * f + offset.y;

       [self openContextualMenuAt:pt];
}

“scrollOffset”, “windowSize”方法是作为UIWebView的category来实现的。“scrollOffset”被要求确保当web页面滚动后坐标也是正确地。“windowSize”返回从HTML文档的角度,HTML文档宽度和高度。所以,基于”HTML window”的windowSize和UIWebView的视图大小,能计算出缩放因子,对于转换屏幕坐标到正确HTML坐标的计算,缩放因子是不可或缺的。

如下是scrollOffset 和 windowSize的实现

WebViewAdditions.h:

@interface UIWebView(WebViewAdditions)
- (CGSize)windowSize;
- (CGPoint)scrollOffset;
@end

WebViewAdditions.m:

#import "WebViewAdditions.h"

@implementation UIWebView(WebViewAdditions)

- (CGSize)windowSize
{
       CGSize size;
       size.width = [[self stringByEvaluatingJavaScriptFromString:@"window.innerWidth"] integerValue];
       size.height = [[self stringByEvaluatingJavaScriptFromString:@"window.innerHeight"] integerValue];
       return size;
}

- (CGPoint)scrollOffset
{
       CGPoint pt;
       pt.x = [[self stringByEvaluatingJavaScriptFromString:@"window.pageXOffset"] integerValue];
      pt.y = [[self stringByEvaluatingJavaScriptFromString:@"window.pageYOffset"] integerValue];
       return pt;
}
@end

最后,需要为UIWebView实现”openContextualMenuAt:”方法,该方法先会检查触碰位置的HTML元素,然后创建上下文菜单。检查触碰位置的HTML元素必须通过JavaScript…

JSTools.js

function MyAppGetHTMLElementsAtPoint(x,y) {
       var tags = ",";
       var e = document.elementFromPoint(x,y);
       while (e) {
          if (e.tagName) {
             tags += e.tagName + ',';
          }
          e = e.parentNode;
       }
       return tags;
}

这个JavaScript函数只是在触碰坐标处简单的收集HTML元素的tag名称,并把tag名称作为一个字符串链返回。JavaScript文件作为资源文件增加到XCode工程中。如果没有作为资源文件添加,XCode有可能把JavaScript文件作为正常的代码来编译和连接。所以确保JavaScript文件中”Copy Bundle Resources”段中,而不是在”Compile Sources”或”Link Binaries”段中。

- (void)openContextualMenuAt:(CGPoint)pt
{
       // Load the JavaScript code from the Resources and inject it into the web page
       NSString *path = [[NSBundle mainBundle] pathForResource:@"JSTools" ofType:@"js"];
       NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
       [webView stringByEvaluatingJavaScriptFromString: jsCode];

       // get the Tags at the touch location
       NSString *tags = [webView stringByEvaluatingJavaScriptFromString:
        [NSString stringWithFormat:@"MyAppGetHTMLElementsAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];

       // create the UIActionSheet and populate it with buttons related to the tags
       UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"Contextual Menu"
              delegate:self cancelButtonTitle:@"Cancel"
              destructiveButtonTitle:nil otherButtonTitles:nil];

       // If a link was touched, add link-related buttons
       if ([tags rangeOfString:@",A,"].location != NSNotFound) {
          [sheet addButtonWithTitle:@"Open Link"];
          [sheet addButtonWithTitle:@"Open Link in Tab"];
          [sheet addButtonWithTitle:@"Download Link"];
       }
       // If an image was touched, add image-related buttons
       if ([tags rangeOfString:@",IMG,"].location != NSNotFound) {
          [sheet addButtonWithTitle:@"Save Picture"];
       }
       // Add buttons which should be always available
       [sheet addButtonWithTitle:@"Save Page as Bookmark"];
       [sheet addButtonWithTitle:@"Open Page in Safari"];

       [sheet showInView:webView];
       [sheet release];
}

这个方法在web页面插入了在触碰位置查找HTML元素的JavaScript方法并调用该函数。返回以逗号分隔的tag名称。字符串以逗号开始并以逗号结束,所以可以简单的检查”,tagName,”的出现,来判断某一特定的tag名称是否被触碰到了。在此例中,如果一个”A”tag被触碰到,只是增加了简单按钮,如果”IMG”tag被触碰到,增加其他一些按钮,增加什么按钮,这取决于你,在iCab Mobile中,只是返回A和IMG的HREF和SRC属性,这样URLS能够被直接的处理。

这个例子中没有包含UIActionSheet委托方法,当你点击上下文菜单中按钮,会调用该委托方法。但是,我想你应该清楚怎么处理这了。另外,其他一些细节也缺失着,但是我想,根据本篇博文,你应该能够在UIWebView中实现你自已的定制化上下文菜单了。

翻译自:customize the contextual menu of UIWebView. 版权所有, 转载请著明出处,保留链接。