移動端(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回傳資料做下一步操作。相當於待人接物而言你叫我做一件事,做好了還是做不了,我得告知你一下,做個有交代有責任的人。
這篇好像有點長,寫得不好的地方還多請見諒,也可在評論指導一下。