1. 程式人生 > >移動端(ios and android)長按識別二維碼(含js與原生互調)

移動端(ios and android)長按識別二維碼(含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回傳資料做下一步操作。相當於待人接物而言你叫我做一件事,做好了還是做不了,我得告知你一下,做個有交代有責任的人。

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