寫在開頭
需求方:上傳試卷的時(shí)候忠烛,用戶自己拍的照片有很多問題属提。如:不清晰、圖片歪了美尸、錯(cuò)誤圖片等冤议。我們要是能夠?qū)ε臄z照片進(jìn)行識別處理就好了,能夠裁切矯正就更好了师坎,最好可以像二維碼掃描一樣恕酸,直接識別處理~
開發(fā):滿足你!
整體框架邏輯
試卷掃描模塊胯陋,最核心的邏輯就是數(shù)據(jù)采集蕊温、解碼識別、圖片裁切遏乔,再加上對識別結(jié)果和裁切結(jié)果的處理义矛,就構(gòu)成了整個(gè)模塊的主邏輯。整個(gè)邏輯的實(shí)現(xiàn)如下圖所示:
在模塊中盟萨,除了UI線程凉翻,還開啟了一個(gè)Deocde線程,用來處理圖片的解碼識別和裁切捻激。這么做的原因是因?yàn)閷τ趫D片數(shù)據(jù)的處理制轰,是比較耗時(shí)的前计,如果在UI線程處理,會(huì)有ANR的風(fēng)險(xiǎn)艇挨。同時(shí)采用這種處理方式残炮,整個(gè)模塊的流暢性也更加好,且模塊的結(jié)構(gòu)更加清晰缩滨。
那么線程之間是如何交互的呢势就?這里模塊中是采用了最常用的Handler消息傳遞機(jī)制。因?yàn)橥ㄟ^Handler的Message可以在線程間傳遞較大的圖片數(shù)據(jù)(注意如果在Intent的Bundle中傳遞較大的數(shù)據(jù)脉漏,會(huì)崩潰報(bào)錯(cuò))苞冯。請看下面這段代碼:
@Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity);
handlerInitLatch.countDown();
Looper.loop();
}
上面這個(gè)方法是DecodeThread的run方法,在方法中侧巨,我們初始化了當(dāng)前線程對應(yīng)的Handler對象DecodeHandler舅锄。而DecodeHandler初始化是需要傳入當(dāng)前主線程的上下文activity,通過activity我們可以拿到主線程的Handler對象司忱。這樣的話主線程和解碼線程就建立了聯(lián)系皇忿,它們之間就可以方便得進(jìn)行消息傳遞了。最終實(shí)現(xiàn)的模塊采集界面如下所示:
模塊開發(fā)相關(guān)實(shí)現(xiàn)
整個(gè)掃碼拍照模塊的邏輯比較瑣碎坦仍,就不一一說明了鳍烁。以下是整理的幾個(gè)開發(fā)中比較關(guān)鍵的點(diǎn)和Camera硬件開發(fā)一些經(jīng)驗(yàn),在這里做記錄繁扎,避免以后重復(fù)造輪子幔荒。
閃光燈設(shè)置
- 開啟閃光燈
public void turnOnFlash(){
if(camera != null){
try {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 關(guān)閉閃光燈
public void turnOffFlash(){
if(camera != null){
try {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
}
預(yù)覽圖片分辨率選擇
預(yù)覽圖片的分辨率選擇邏輯是:有1920*1080則選之,否則選硬件支持的最大的分辨率梳玫,且滿足圖片比例為16:9
private static Point findBestPreviewSizeValue(List<Camera.Size> sizeList, Point screenResolution) {
int bestX = 0;
int bestY = 0;
int size = 0;
for(int i = 0; i < sizeList.size(); i ++){
// 如果有符合的分辨率爹梁,則直接返回
if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
Log.d(TAG, "get default preview size!!!");
return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
int newX = sizeList.get(i).width;
int newY = sizeList.get(i).height;
int newSize = Math.abs(newX * newX) + Math.abs(newY * newY);
float ratio = (float)newY / (float)newX;
Log.d(TAG, newX + ":" + newY + ":" + ratio);
if (newSize >= size && ratio != 0.75) { // 確保圖片是16:9的
bestX = newX;
bestY = newY;
size = newSize;
} else if (newSize < size) {
continue;
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}
return null;
}
拍照圖片分辨率選擇
在硬件支持的拍照圖片分辨率列表中,拍照圖片分辨率選擇邏輯:
- 有1920*1080則選之
- 選擇大于屏幕分辨率且圖片比例為16:9的
- 選擇圖片分辨率盡可能大且圖片比例為16:9的
private static Point findBestPictureSizeValue(List<Camera.Size> sizeList, Point screenResolution){
List<Camera.Size> tempList = new ArrayList<>();
for(int i = 0; i < sizeList.size(); i ++){
// 如果有符合的分辨率提澎,則直接返回
if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
Log.d(TAG, "get default picture size!!!");
return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
if(sizeList.get(i).width >= screenResolution.x && sizeList.get(i).height >= screenResolution.y){
tempList.add(sizeList.get(i));
}
}
int bestX = 0;
int bestY = 0;
int diff = Integer.MAX_VALUE;
if(tempList != null && tempList.size() > 0){
for(int i = 0; i < tempList.size(); i ++){
int newDiff = Math.abs(tempList.get(i).width - screenResolution.x) + Math.abs(tempList.get(i).height - screenResolution.y);
float ratio = (float)tempList.get(i).height / tempList.get(i).width;
Log.d(TAG, "ratio = " + ratio);
if(newDiff < diff && ratio != 0.75){ // 確保圖片是16:9的
bestX = tempList.get(i).width;
bestY = tempList.get(i).height;
diff = newDiff;
}
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}else {
return findMaxPictureSizeValue(sizeList);
}
}
預(yù)覽模式循環(huán)自動(dòng)對焦
預(yù)覽模式時(shí)姚垃,支持自動(dòng)對焦。當(dāng)前處理邏輯是在AutoFocusCallback的回調(diào)方法onAutoFocus中盼忌,延遲發(fā)送Message信息积糯。這樣在上一次聚焦完成后,固定時(shí)間的延遲后會(huì)發(fā)送下一次的自動(dòng)聚焦消息碴犬,如此達(dá)到循環(huán)聚焦的目的。
@Override
public void onAutoFocus(boolean success, Camera camera) {
Log.d(TAG, "onAutoFocus");
PaperScanConstant.isAutoFocusSuccess = true;
if (autoFocusHandler != null) {
Message message = autoFocusHandler.obtainMessage(autoFocusMessage, success);
autoFocusHandler.sendMessageDelayed(message, AUTOFOCUS_INTERVAL_MS);
autoFocusHandler = null;
} else {
Log.d(TAG, "Got auto-focus callback, but no handler for it");
}
}
預(yù)覽畫面不失真展示
如果預(yù)覽圖片的分辨率比例和手機(jī)畫面上展示拍攝畫面的區(qū)域比例不一致的話梆暮,就會(huì)出現(xiàn)畫面拉伸或者壓縮的現(xiàn)象服协。為了解決這個(gè)問題,取得更好的用戶體驗(yàn)啦粹。模塊在布局的時(shí)候偿荷,對屏幕展示區(qū)域是動(dòng)態(tài)計(jì)算的窘游,以保證預(yù)覽區(qū)域比例與圖片的分辨率比例是一致的。
模塊開發(fā)中的那些坑
掃碼模塊開發(fā)跳纳,因?yàn)槭歉謾C(jī)硬件Camera打交道忍饰,基于目前市場中Android手機(jī)眾多的型號和搭載的五花八門的ROM,沒坑那是不可能的K伦0丁!下面是本模塊開發(fā)過程中的相關(guān)坑斗塘。
部分機(jī)子拍攝照片分辨率不高
開發(fā)過程中碰到過這么一種情況赢织,在部分機(jī)子上,明明已經(jīng)聚焦馍盟,手機(jī)的分辨率也很高于置,但是拍出的照片分辨率卻很小。究其原因贞岭,就是不同的手機(jī)ROM八毯,獲取的默認(rèn)的照片分辨率是不同的。有的手機(jī)默認(rèn)照片分辨率高瞄桨,則照片就清晰话速;有的默認(rèn)分辨率是最低的一檔,則無論你手機(jī)分辨率多高讲婚,拍出來的照片還是很模糊的尿孔。解決方案就是需要顯示設(shè)置拍照的圖片分辨率:
parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
部分機(jī)子拍攝照片發(fā)生了旋轉(zhuǎn)
還是由于Android手機(jī)碎片化的問題,每個(gè)手機(jī)默認(rèn)拍照的旋轉(zhuǎn)角度是不一樣的筹麸。剛開始模塊中是按照默認(rèn)旋轉(zhuǎn)90度處理活合,在大多數(shù)機(jī)子上是沒有問題的。但是在碰到Nexus 5X的時(shí)候就出問題了物赶,圖片上下導(dǎo)致了白指。查閱了相關(guān)資料,Google官方提供了下面的方法酵紫,解決了這個(gè)問題告嘲。
public void setCameraDisplayOrientation(int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = BaseApplication.getInstance().getCurrentActivity().getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
// 記錄本機(jī)子相機(jī)的旋轉(zhuǎn)角度
PaperScanConstant.cameraRotation = result;
camera.setDisplayOrientation(result);
}
private int findFrontFacingCameraID() {
int cameraId = -1;
// Search for the back facing camera
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
Log.d(TAG, "Camera found");
cameraId = i;
break;
}
}
return cameraId;
}
頻繁點(diǎn)擊屏幕應(yīng)用崩潰
因?yàn)閼?yīng)用支持點(diǎn)擊屏幕自動(dòng)聚焦功能,但在某些機(jī)子上奖地,用戶頻繁點(diǎn)擊屏幕進(jìn)行自動(dòng)聚焦橄唬,應(yīng)用發(fā)生了崩潰。究其原因是因?yàn)樵谀承㏑OM上参歹,當(dāng)上一次聚焦沒有完成時(shí)仰楚,就進(jìn)行下一次聚焦,就會(huì)發(fā)生崩潰。解決方案是通過設(shè)置標(biāo)志位僧界,只有在上一次聚焦完成后侨嘀,才能進(jìn)行下一次聚焦。
第三發(fā)ROM禁止了應(yīng)用的攝像頭權(quán)限
有些第三方ROM會(huì)有自己的權(quán)限管理機(jī)制捂襟,當(dāng)應(yīng)用的攝像頭權(quán)限被禁止了咬腕,進(jìn)入掃碼頁,會(huì)發(fā)生崩潰葬荷。這樣的交互體驗(yàn)肯定不是很好涨共,交互要求這邊權(quán)限被禁止以后,還是需要有一個(gè)溫和的提示闯狱,提醒用戶去設(shè)置頁面重新賦予應(yīng)用攝像頭權(quán)限煞赢。但是系統(tǒng)也沒有提供接口說當(dāng)前應(yīng)用這個(gè)權(quán)限被禁止了。因此模塊中采用了一個(gè)折中的方案哄孤,監(jiān)獄應(yīng)用沒有攝像頭權(quán)限時(shí)候照筑,開啟攝像頭會(huì)崩潰。因此我們捕獲開啟Camera的異常瘦陈,在捕獲異常時(shí)候彈框提醒用戶去開啟權(quán)限凝危。
try {
CameraManager.get().openDriver(surfaceHolder);
} catch (Throwable tr){
showOpenCameraErrorDialog();
return;
}
Pad進(jìn)入掃碼頁應(yīng)用崩潰
實(shí)際上線時(shí)候,發(fā)現(xiàn)用戶使用pad的話晨逝,一進(jìn)入掃碼頁面就崩潰蛾默。因?yàn)槲覀儜?yīng)用首次進(jìn)入掃碼頁面默認(rèn)是開啟設(shè)備閃光燈的。但是pad沒有閃光燈捉貌,因此就崩潰了支鸡。剛開始用如下方式檢測設(shè)備是否支持閃光燈:
getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
但是失敗了。原因是好多pad的ROM是從手機(jī)ROM改過去的趁窃,有可能改得不是那么徹底牧挣。所以在Pad上調(diào)用如上代碼進(jìn)行判斷時(shí),還是會(huì)返回true醒陆。這是只能求助于try catch了瀑构。就是在開關(guān)閃光燈的時(shí)候進(jìn)行異常捕獲,這樣在Pad上開關(guān)閃光燈崩潰問題就解決了刨摩。
部分機(jī)子拍照后閃光燈自動(dòng)關(guān)閉
部分機(jī)子寺晌,在閃光燈開啟的狀態(tài)下,點(diǎn)擊拍照按鈕澡刹,閃光燈關(guān)閉了呻征。目前沒有找到原因,只能在模塊中加了特殊處理罢浇。針對當(dāng)前有此問題的手機(jī)陆赋,拍照完后主動(dòng)再去開關(guān)一次閃光燈边篮,這樣拍照完成后,閃光燈還是可以亮著奏甫。只是在拍照的過程中,會(huì)出現(xiàn)閃光燈閃爍的情況凌受。
部分機(jī)子拍照完后預(yù)覽畫面卡住了
部分機(jī)子阵子,當(dāng)點(diǎn)擊拍照完成一張照片的拍攝后,后面就停止不動(dòng)了胜蛉。出現(xiàn)這種現(xiàn)象是因?yàn)樵谂恼盏臅r(shí)候挠进,Camera會(huì)停止Preview,拍照完成后誊册,有的機(jī)子可以恢復(fù)回來重新Preview领突,有的則不會(huì)。因此只需在拍照完成后案怯,手動(dòng)調(diào)用一次Camera的startPreview()方法即可君旦。
結(jié)束語
最后,大家想看代碼的話嘲碱,可以看下我封裝的二維碼掃描庫金砍,實(shí)現(xiàn)原理是一樣的÷缶猓可以看我這篇文章:一款好用的二維碼掃描組件
二維碼掃描庫QrScan的GitHub:https://github.com/yushiwo/QrScan