WebView自定義長按圖片功能

Garrett Gao發表於2019-03-13

關於Webview長按圖片功能,系統預設自帶選單彈窗,但是某些場景我們需要自定義選單功能,此時就需要遮蔽系統彈窗,實現自己的彈窗方式。 因為iOS12之後 UIWebview蘋果將要廢棄,所以這裡以 WKWebview舉例說明。

下面介紹幾個用到JS程式碼:

遮蔽系統彈窗

// 當長按時,禁止或顯示系統預設選單
document.documentElement.style.webkitTouchCallout='none';
//當長按時,禁止選擇內容
document.documentElement.style.webkitUserSelect='none';
複製程式碼

通過座標獲取某個HTML標籤元素

// 通過座標獲取某個位置的元素
let element = document.elementFromPoint(x,y);
複製程式碼

判斷標籤元素是否包含某個屬性

// 是否包含 app-press-disabled 屬性,如果包含,說明該標籤不允許長按事件(我們自定義協定)
let isCanLong = element.getAttribute("app-press-disabled") == null;
複製程式碼

考慮部分場景不需要長按圖片的功能,所以可以通過一個協定好的屬性來當做長按開關,比如我們用 app-press-disabled 屬性來標示禁止某個元素長按手勢開啟,當 html標籤中存在 app-press-disabled 屬性,如:<img src='https://image_url' app-press-disabled>,此圖片長按無效(具體規則可以自己定義)。

獲取元素標籤名稱判斷是否是某個標籤

// 是否是圖片 IMG標籤
 let isImgTag = element.tagName.toLowerCase() == "img";
複製程式碼

好,以上js程式碼夠我們完成功能,我們將以上程式碼寫入一個js檔案:

// JavaScript 檔案 GGWebLongPressImage.js
// 本段js用於webview長按圖片功能設計
//

// 關閉webview自帶的長按事件和彈窗
document.documentElement.style.webkitTouchCallout='none';
document.documentElement.style.webkitUserSelect='none';

// 判斷圖片是否可以觸發長按事件指令碼
// 引數:point座標
// 返回:String,如果識別圖片返回圖片地址,否則返回 "not_image"
function app_isLongPressImageWithPoint(x,y) {

    // 通過座標獲取某個位置的元素
    let element = document.elementFromPoint(x,y);

    // 是否包含 app-press-disabled 屬性,如果包含,說明該標籤不允許長按事件(我們自定義協定)
    let isCanLong = element.getAttribute("app-press-disabled") == null;

    // 是否是 IMG標籤
    let isImgTag = element.tagName.toLowerCase() == "img";

    if (isCanLong && isImgTag) {
        return element.src;
    }else {
        return "not_image";
    }
}

複製程式碼

JS指令碼程式碼準備完畢,效果為如果長按座標位置為圖片返回圖片URL,如果不為圖片或者不滿足長按條件,返回 "not_image"。 為了儘量解耦程式碼,我們把此功能單獨寫到WKWebview 的分類中。建立分類:

WKWebView+LongPress.h
複製程式碼

為了保證每個頁面此段js生效,我們將js程式碼插入WKWebview的 userScripts,保證每個頁面指令碼程式碼生效。

/// 加入JS指令碼程式碼
- (void)addJsCode {
    //獲取網頁的根域名
    NSString *jsCode = [WKWebView loadJsCodeWithFileName:@"GGWebLongPressImage" withType:@"js"];
    if (jsCode) {
        WKUserScript *cookieInScript = [[WKUserScript alloc] initWithSource:jsCode injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
        [self.configuration.userContentController addUserScript:cookieInScript];
    }
}

/// 讀取本地js檔案
+ (NSString *)loadJsCodeWithFileName:(NSString *)name withType:(NSString *)type {
    NSString *filePath = [[NSBundle mainBundle] pathForResource:name ofType:type];
    NSString *jsCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    return jsCode;
}
複製程式碼

新增長按手勢:

/// 新增長按手勢
- (void)addLongPressGesture {
    // 新增長按手勢
    UILongPressGestureRecognizer *longGes = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPressHandler:)];
    longGes.cancelsTouchesInView = NO;
    longGes.delegate = self;
    [self addGestureRecognizer:longGes];
    
    // 植入js指令碼
    [self addJsCode];
}
複製程式碼

開啟webview多手勢開關,前提判斷如果手勢為長按事件

/// 多手勢開關
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    //只有當手勢為長按手勢時反饋,飛長按手勢將阻止。
    return [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]];
}
複製程式碼

長按手勢selector實現:

// 長按手勢觸發呼叫
- (void)onLongPressHandler:(UILongPressGestureRecognizer *)longPress {
    
    if (longPress.state == UIGestureRecognizerStateBegan) {
                
        CGPoint pt = [longPress locationInView:self];
        
        // 執行剛才的js程式碼,判斷是否滿足長按需求並且為圖片
        NSString *checkLongJs = [NSString stringWithFormat:
                                 @"app_isLongPressImageWithPoint(%f,%f)",
                                 pt.x,pt.y];
        
        
        // 執行拼接好的指令碼程式碼
        [self evaluateJavaScript:checkLongJs completionHandler:^(NSString* callBackString, NSError * _Nullable error) {
            imageUrl = callBackString;
            
            if(![imageUrl isEqualToString:@"not_image"]) { //滿足長按圖片條件
              //拿到圖片的url,業務程式碼處理
            }
        }];
    }
}
複製程式碼

以上基本完成長按圖片的功能。

但是我們發現長按圖片雖然生效,但是鬆手的時候,如果圖片有其他點選響應,點選事件也被觸發,頁面會載入。

我們這裡通過延時處理解決的這個問題: 通過一個屬性判斷是否為長按事件,如果為長按事件,手指離開螢幕時,禁止頁面跳轉,完整程式碼如下:

WKWebView+LongPress.h

#import <WebKit/WebKit.h>

// 長按協議
@protocol WKLongPressDelegate <NSObject>
- (void)webViewOnLongPressHandlerWithWebView:(WKWebView *)webView withImageUrl:(NSString *)imageUrl;
@end

@interface WKWebView (LongPress)<UIGestureRecognizerDelegate>

/// 長按手勢代理
@property (nonatomic, weak) id<WKLongPressDelegate> longPressDelegate;

/**
 是否可以跳轉,主要用於解決長按事件後,頁面再次跳轉問題
 *  YES: 不可以,NO可以
 */
@property (nonatomic, assign)  BOOL isNotPushLink;


/**
 新增長按手勢
 */
- (void)addLongPressGesture;

@end
複製程式碼

WKWebView+LongPress.m

#import "WKWebView+LongPress.h"
#import <objc/runtime.h>

@implementation WKWebView (LongPress)

#pragma mark @property -setter getter
@dynamic longPressDelegate;

- (void)setLongPressDelegate:(id<WKLongPressDelegate>)longPressDelegate {
    objc_setAssociatedObject(self, @selector(longPressDelegate), longPressDelegate, OBJC_ASSOCIATION_ASSIGN);
}

- (id)longPressDelegate {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setIsNotPushLink:(BOOL)isNotPushLink {
    objc_setAssociatedObject(self, @selector(isNotPushLink), [NSNumber numberWithBool:isNotPushLink], OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)isNotPushLink {
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
#pragma mark - End

/**
 新增長按手勢
 */
- (void)addLongPressGesture {
    // 新增長按手勢
    UILongPressGestureRecognizer *longGes = [[UILongPressGestureRecognizer alloc] initWithTarget:self
                                                                                          action:@selector(onLongPressHandler:)];
    longGes.cancelsTouchesInView = NO;
    longGes.delegate = self;
    [self addGestureRecognizer:longGes];
    // 植入js指令碼
    [self addJsCode];
}

/**
 加入JS指令碼程式碼
 */
- (void)addJsCode {
    //獲取網頁的根域名
    NSString *jsCode = [WKWebView loadJsCodeWithFileName:@"GGWebLongPressImage" withType:@"js"];
    if (jsCode) {
        WKUserScript *cookieInScript = [[WKUserScript alloc] initWithSource:jsCode
                                                              injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                           forMainFrameOnly:NO];
        [self.configuration.userContentController addUserScript:cookieInScript];
    }
}

/// 是否實現了長按代理
- (BOOL)isOpenLongPressDelegate {
    return (self.longPressDelegate && [self.longPressDelegate respondsToSelector:@selector(webViewOnLongPressHandlerWithWebView:withImageUrl:)]);
}


/// 多手勢開關
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    //只有當手勢為長按手勢時反饋,飛長按手勢將阻止。
    return [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]];
}

// 長按手勢觸發呼叫
- (void)onLongPressHandler:(UILongPressGestureRecognizer *)longPress {
    
    if (![self isOpenLongPressDelegate]) {
        return;
    }
    
    if (longPress.state == UIGestureRecognizerStateBegan) {
        
        self.isNotPushLink = YES;
        
        __weak typeof(self) weakSelf = self;
        __block NSString *imageUrl = nil;
        
        CGPoint pt = [longPress locationInView:self];
        
        // 判斷是否滿足長按需求
        NSString *checkLongJs = [NSString stringWithFormat:
                                 @"app_isLongPressImageWithPoint(%f,%f)",
                                 pt.x,pt.y];
        
        
        // 如果圖片有點選事件,不觸發長按響應
        [self evaluateJavaScript:checkLongJs completionHandler:^(NSString* callBackString, NSError * _Nullable error) {
            imageUrl = callBackString;
            
            if(![imageUrl isEqualToString:@"not_image"]) { //滿足長按圖片條件
                if ([weakSelf isOpenLongPressDelegate]) {
                    [weakSelf.longPressDelegate webViewOnLongPressHandlerWithWebView:self withImageUrl:imageUrl];
                }
            }
        }];
        
    } else if(longPress.state == UIGestureRecognizerStateEnded ||
              longPress.state == UIGestureRecognizerStateCancelled ||
              longPress.state == UIGestureRecognizerStateFailed) {
        //延時0.2秒後,取消webview不可跳轉連結狀態,解決長按跳轉問題
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            self.isNotPushLink = NO;
        });
    }
}

/**
 讀取本地js檔案
 
 @param name 檔名稱
 @param type 檔案型別
 @return 返回js程式碼
 */
+ (NSString *)loadJsCodeWithFileName:(NSString *)name withType:(NSString *)type {
    
    NSError *error = nil;
    NSString *filePath = [[NSBundle mainBundle] pathForResource:name ofType:type];
    NSString *jsCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
    if (error) { NSLog(@"讀取js檔案失敗:error:%@",error); }
    return jsCode;
}

@end
複製程式碼

這個分類完成長按圖片的所有功能。

如何使用?

#import "WKWebView+LongPress.h"

//實現 WKLongPressDelegate 代理

//初始化 WKWebView
  WKWebView* webView = ....;

// 實現長按web圖片代理
  webView.longPressDelegate = self;
// 新增長按手勢
  [webView addLongPressGesture];

//實現WKNavigationDelegate方法
- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{

  // 如果為長按操作,中斷頁面載入
    if (webView.isNotPushLink) { decisionHandler(WKNavigationActionPolicyCancel); return; }
    
    decisionHandler(WKNavigationActionPolicyAllow);
}

// 實現長按webview中的圖片代理,當滿足自定義條件後觸發
- (void)webViewOnLongPressHandlerWithWebView:(WKWebView *)webView withImageUrl:(NSString *)imageUrl {
    
    // 處理長按圖片業務程式碼, 如彈框展示 儲存圖片、識別二維碼 等
}

複製程式碼

方式雖然簡單暴力,但是確實能解決長按跳轉的燃眉之急。 UIWebView也是同樣邏輯處理即可。

相關文章