移動端(ios and android)長按識別二維碼(含js與原生互調)

這篇文章就整理下移動端長按識別二維碼的實現吧!實現方式可以分爲三種

一、長按原生控件,直接獲取控件中的圖片數據(src或background)

二、長按原生控件,截圖識別

三、長按web中的圖片,app識別其中的二維碼(js互調)


第一二種好像沒多少可以說的,但還是按照順序來吧!首先先說下使用的庫,ios使用原生二維碼識別庫(好像是ios7之後纔有的),然後說是WKWebView比UIWebView優化了很多 東西,也解決了內存泄漏的問題那麼js交互的部分我們就用WKWebView吧(說到這裏必須吐槽下android的webView內存泄漏問題,一個字坑)。android沒原生的,我瞭解的比較大衆的就zxing和zbar,經過測試發現在二維碼佔圖片的比例較小識別時zbar的識別比zxing好一些,而且zxing使用截圖的方案實現時當二維碼放在屏幕的底部時識別不出來,所以這裏就直接只貼zbar的代碼吧!

然後呢因爲是寫的demo,代碼是沒優化過的,怎麼方便怎麼來,實際使用還是得根據自己的需要優化一下,個人覺得重要的是實現方案和思路。

一、長按原生控件,直接獲取控件中的圖片數據(src或background)

這裏基本上等於在介紹,二維碼識別的使用了。

(1)android

獲取圖片android就比較簡單了。長按事件就不說了,圖片通常會用ImageView,直接獲取src就行了,特殊點的放background,那麼就獲取background就好了。直接貼代碼吧!

//src
Bitmap mBitmap=((BitmapDrawable) imageView.getDrawable()).getBitmap();

//background
mBitmap=((BitmapDrawable) imageView.getBackground()).getBitmap();

下面就是關鍵zbar 識別圖片中二維碼的代碼了

-----------------------------------------------
public String parseRQ(Bitmap bitmap) {
        String text = null;
        ImageScanner scanner=new ImageScanner();
        scanner.setConfig(0, Config.X_DENSITY,3);
        scanner.setConfig(0, Config.Y_DENSITY, 3);
        //設置掃描的圖片
        Image barcode = new Image(bitmap.getWidth(), bitmap.getHeight(), "Y800");
        //設置掃描的圖片的區域,因爲我們不知道二維碼在哪,所以直接設置整張圖片
        barcode.setCrop(0, 0, bitmap.getWidth(), bitmap.getHeight());
        int[] data = new int[bitmap.getWidth() * bitmap.getHeight()];
        byte[] bitmapPixels =
                new byte[bitmap.getWidth() * bitmap.getHeight()];
        bitmap.getPixels(data, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());

        for (int i = 0; i < data.length; i++) {
            bitmapPixels[i] = (byte) data[i];
        }
        barcode.setData(bitmapPixels);
        //識別圖片中的二維碼,result是二維碼的個數(這點比zxing好,zxing只獲取從左上開始找到的第一個,不過也有可能是我調用的api不對也不一定)
        int result = scanner.scanImage(barcode);
        if (result != 0)
        {
            SymbolSet syms = scanner.getResults();
            for (Symbol sym : syms)
            {
                text=sym.getData().trim();
                //我們只獲取第一個非空二維碼,習慣性判空,沒測過幾個空字符串可不可以生成二維碼
                if(!text.isEmpty())
                {
                    break;
                }
            }
        }

        return text;

    }

--------------------------------------------------

拿到解碼後的數據,就可以根據需求取實現功能了。

(2)ios

ios獲取UIImageView的圖片更容易直接就是imageView.image就可以了,原生識別二維碼的操作也簡單,個人覺得設置長按事件比這兩個加起來都麻煩點。所以這裏主要就是設置長按事件的代碼了。貼碼。

//創建長按,imageLongClick即爲長按響應的函數
UILongPressGestureRecognizer *longClick=[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(imageLongClick:)];
    //觸摸點數,即多少手指點擊
    longClick.numberOfTouchesRequired=1;
    //開啓觸發事件處理
    imageView.userInteractionEnabled=YES;
    //imageView添加長按事件
    [imageView addGestureRecognizer:longClick];

ios原生識別二維碼,比zxing和zbar都簡單多了,當然這沒做優化策略的,android zbar那那個代碼也一樣

----------------------------------------------
-(void)imageLongClick:(UILongPressGestureRecognizer *)sender{
    //按下時
    if ([sender state]==UIGestureRecognizerStateBegan) {
        NSLog(@"image long click....");
        //創建識別器
        CIDetector *detector=[CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:nil];
        //image轉爲CGImage進行識別,結果爲所有二維碼結果的對象數組
        NSArray *results=[detector featuresInImage:[CIImage imageWithCGImage:[self snapshotView].CGImage]];
        if (results.count>0) {
            //這裏只拿第一個
            CIQRCodeFeature *feature=[results firstObject];
            //feature.messageString即爲解碼後的字符串,這裏直接打開瀏覽器
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:feature.messageString]];
        }else{
            NSLog(@"找不到二維碼");
        }
    }
}
--------------------------------------------


在此直接獲取控件的圖片直接識別的就這樣結束了,如果要獲取相冊的也一樣,只需將從相冊獲取到的圖片轉爲對應的Bitmap(ios UIImage),其他的都不變就可。

這裏需注意的是背景色如果是透明色是無法識別出來的,所以如果二維碼的來源是自己app的這種方式就很好了,如果是用戶上傳的建議用第二種方式,不可保證不會有哪個坑上傳個透明背景的圖片或上傳個長圖。


二、長按原生控件,截圖識別

長按事件和識別二維碼的代碼是一樣的,就不重複,即獲取到截屏的圖片後調用識別的方法就可以了,所以這裏就只剩截屏功能的代碼了,一樣直接上代碼

(1)android

-----------------------------------------------------
//其實這裏直接傳個View進來也是可以的,比如第三種的長按網頁的就可以將WebView傳就來就可以了
public Bitmap snapshotView(Window window) {
        if (window != null) {
            //找到當前頁面的根佈局
            View view = window.getDecorView().getRootView();
            //獲取當前屏幕的大小
            int width = view.getWidth();
            int height = view.getHeight();

            //設置緩存
            view.setDrawingCacheEnabled(true);
            view.buildDrawingCache();
            /*1、從緩存中獲取當前屏幕的圖片,創建一個DrawingCache的拷貝,因爲DrawingCache得到的位圖在禁用後會被回收
             *2、這裏的88是去掉無用的部分即你確定是不會有二維碼的部分(當然不做任何操作也是可以的),這裏直接寫死是狀態欄的高度,
             *實際真正使用不會這麼寫,而是是去獲取狀態欄的高度(這裏懶就不寫了),我記得如果直接是控件調用buildDrawingCache
             *是該控件當前顯示在屏幕上的部分就不用減去狀態欄的高度了
             */
            Bitmap temBitmap = Bitmap.createBitmap(view.getDrawingCache(), 0, 88, width, height - 88);
            //禁用DrawingCahce否則會影響性能 ,而且不禁止會導致每次截圖到保存的是緩存的位圖
            view.destroyDrawingCache();
            view.setDrawingCacheEnabled(false);

            return temBitmap;
        }
        return null;
    }
---------------------------------------------------

(2)ios

---------------------------------------------
//跟android一樣這裏的UIWindow也可以改成UIView,函數接受UIView的參數,外部就可以直接調用截取指定控件顯示在屏幕的部分截圖了
- (UIImage *)snapshotView {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    //這裏只獲取大小,所以bounds還是frame都是一樣的
    CGRect rect = [keyWindow bounds];
    if(UIGraphicsBeginImageContextWithOptions != NULL)
    {
        //iphone4之後採用Retina屏幕調用這個(不知道有沒有記反,也有其他的截圖方式,只是我就記得這種)
        UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
    } else {
        UIGraphicsBeginImageContext(rect.size);
    }
    CGContextRef context = UIGraphicsGetCurrentContext();
    [keyWindow.layer renderInContext:context];
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
}
-----------------------------------------------------------------------------------------------


三、長按web中的圖片,app識別其中的二維碼

這裏也是截圖實現,爲什麼不是拿原始圖片識別,1是上面說的有可能是長圖或背景透明,2是截圖免下載,在速度體驗上好點。我記得微信也是這樣實現的,在哪提過我忘了。

既然涉及js,那我們就必須先來段js呀,js長按圖片功能代碼(本來是想用jquery的,但想想網頁不一定是自己的,有可能是別人的靜態網頁,根本沒導入jquery庫,而直接 注入<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>的形式是會有跨域的問題的,所以不用jquery,直接寫好了,代碼量其實也差不多):

-----------------------------------------------------
//閉包,這裏個人當成跟java的匿名對象差不多記憶
(function () {
        //獲取所有圖片標籤
        var allImage = document.getElementsByTagName("img");
        var img;
        for (var i = 0; i < allImage.length; i++) {
            img = allImage[i];
            //添加觸摸事件
            img.addEventListener('touchstart', function(event) {
                touch = event.touches[0];
                startevent = event;
            //保存觸摸點的x,y軸
                startX = Number(touch.pageX);
                startY = Number(touch.pageY);
            //設置定時器,js沒長按事件,就是使用定時器實現的,800毫秒後觸發,img.src爲圖片地址,這裏可以拿到後做保存圖片發大圖等功能
                timeout = setTimeout('longClick('+img.src+');', 800);
            });
            //移動事件
            img.addEventListener('touchmove', function(event) {
                touch = event.touches[0];
                scx = Math.abs(Number(touch.pageX) - startX);
                scy = Math.abs(Number(touch.pageY) - startY);
                //過濾掉移動事件,不這樣做,當你手指放在這個圖片往上或往下劃的時候,800毫秒後也會觸發長按事件,精確度可以自己調
                if (scx > 10 || scy > 10) { 
                   //取消定時器
                    clearTimeout(timeout);
                } else {
                    //相當android的攔截分發
                    event.preventDefault();
                }
            });
            //手指放開時取消定時器
            img.addEventListener('touchend', function(event) {
                clearTimeout(timeout);
            });
        }
    })();//立即執行
-------------------------------------------------------

app要做的就是兩件事,1、將截圖識別二維碼對象注入js中。2、網頁加載結束後,加載執行上面那個js函數代碼即可。

(1)android

android相對簡單點,創建注入對象

---------------------------------------------------
public class DemoJSBridge{
      @JavascriptInterface
       public void longClickImage(String imgSrc){
              //調用截圖識別二維碼代碼
       }
}

//注入
wb.addJavascriptInterface(new DemoJSBridge(), "demoJSBridge");

---------------------------------------------------

這樣就可以注入代碼了,其他功能直接在類中加方法即可,而前端js調用則是

//對象是注入到window對象中的,所以是window.對象.方法
window.demoJSBridge.longClickImage(img.src);

所以上面的js函數中的'longClick('+img.src+');'改掉,然後頁面加載完成後注入執行即可。即

------------------------------------------------------
        wb.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
               //加載js,而我們那個js函數是立即執行的,所以一加載就會自動執行,getQRJs()即爲上面js函數的String格式
                wb.loadUrl("javascript:" + getQRJs());
            }
        });
------------------------------------------------------


(2)ios個人覺得麻煩點,但安全點

注入js對象

-------------------------------------------------------------------------
//構建script對象,配置頁面加載完成後加載上面js函數的並執行,getJSString即爲上面js函數的字符串,
//WKUserScriptInjectionTimeAtDocumentEnd頁面加載結束後注入
WKUserScript *script=[[WKUserScript alloc] initWithSource:[self getJSString] injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
    //WKWebView配置對象
    WKWebViewConfiguration *config=[[WKWebViewConfiguration alloc] init];
    config.preferences=[WKPreferences new];
    //允許執行javaScript
    config.preferences.javaScriptEnabled=YES;
    //WKWebView自帶長按事件,會攔截掉我們添加的事件,所以屏蔽掉
    NSMutableString *javascript = [NSMutableString string];
    //禁止webkitTouchCallout
    [javascript appendString:@"document.documentElement.style.webkitTouchCallout='none';"];
    [javascript appendString:@"document.documentElement.style.webkitUserSelect='none';"];//禁止選擇
    WKUserScript *noneSelectScript = [[WKUserScript alloc] initWithSource:javascript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];

    [config.userContentController addUserScript:noneSelectScript];
    [config.userContentController addUserScript: script];
    //注入對象addScriptMessageHandler響應處理者self
    [config.userContentController addScriptMessageHandler:self name:@"demoJSBridge"];
    WKWebView *webView=[[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
---------------------------------------------------------------------------------

然後注意

1、script即我們上面代碼中加載js函數的形式也可以用第二種方式,跟android的差不多,即

-(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
    [self.webView evaluateJavaScript:[self getJSString] completionHandler:nil];
}
 

2、實現協議WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler,第一個頁面加載進度等事件的回調。第二個js對話框的回調,WKWebView把js的對話框就是alert()這些給屏蔽了,我們需實現WKUIDelegate協議自己去彈窗。第三個js調用原生的方法。


3、WKWebView注入的對象跟android和UIWebView都不一樣了,他放在了window.webkit.messageHandlers裏,所以前端js調用時爲

window.webkit.messageHandlers.對象名.postMessage(參數);
所以所以上面的js函數中的'longClick('+img.src+');'改掉,參數直接傳js對象,如{functionName:"longClickImage",data:img.src},實現,如果爲減少與android的差異性,android也可以改爲只有postMessage(String msg)方法,然後根據functionName去執行對應的功能。js對象傳到app後,ios會自動轉爲字典,android爲json字符串,自己轉成json就可以了。

個人理解是WKWebView取消了直接注入對象了,即沒有將self注入到js中,而是於注入一個假對象,裏面只有postMessage函數,當js調用這個函數時他對應的再去調用原生的回調。


4、響應js調用事件,即實現WKScriptMessageHandler協議

------------------------------------------------------------
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
     //message中的name即爲注入的對象名,body爲傳過來的數據
    if ([message.name isEqualToString:@"demoJSBridge"]) {
        //js對象傳過來後會自動轉爲字典
        NSDictionary *d=message.body;
        if ([@"longClickImage" isEqualToString:[d objectForKey:@"functionName"]]) {
            //調用截屏,識別二維碼方法
        }
    }
}
-------------------------------------------------------------------

理論上js與原生的互調就這樣可以了,但爲了保險一點的話,原生接到js的調用以後再回調一下js比較好一點,因爲有可能有些功能js需要app回傳數據做下一步操作。相當於待人接物而言你叫我做一件事,做好了還是做不了,我得告知你一下,做個有交代有責任的人。


這篇好像有點長,寫得不好的地方還多請見諒,也可在評論指導一下。








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章