Android實(shí)現(xiàn)后臺(tái)服務(wù)拍照功能
一、背景介紹
最近在項(xiàng)目中遇到一個(gè)需求,實(shí)現(xiàn)一個(gè)后臺(tái)拍照的功能。一開始在網(wǎng)上尋找解決方案,也嘗試了很多種實(shí)現(xiàn)方式,都沒有滿意的方案。不過確定了難點(diǎn):即拍照要先預(yù)覽,然后再調(diào)用拍照方法。問題也隨之而來,既然是要實(shí)現(xiàn)后臺(tái)拍照,就希望能在Service中或者是異步的線程中進(jìn)行,這和預(yù)覽這個(gè)步驟有點(diǎn)相矛盾。那有什么方式能夠既能正常的實(shí)現(xiàn)預(yù)覽、拍照,又不讓使用者察覺呢?想必大家也會(huì)想到一個(gè)取巧的辦法:隱藏預(yù)覽界面。
說明一下,這只是我在摸索中想到的一種解決方案,能很好的解決業(yè)務(wù)上的需求。對于像很多手機(jī)廠商提供的“找回手機(jī)”功能時(shí)提供的拍照,我不確定他們的實(shí)現(xiàn)方式。如果大家有更好的實(shí)現(xiàn)方案,不妨交流一下。
關(guān)于這個(gè)功能是否侵犯了用戶的隱私,影響用戶的安全等等問題,不在我們的考慮和討論范圍之內(nèi)。
二、方案介紹
方案實(shí)現(xiàn)步驟大致如下:
1.初始化拍照的預(yù)覽界面(核心部分);
2.在需要拍照時(shí)獲取相機(jī)Camera,并給Camera設(shè)置預(yù)覽界面;
3.打開預(yù)覽,完成拍照,釋放Camera資源(重要)
4.保存、旋轉(zhuǎn)、上傳.......(由業(yè)務(wù)決定)
先大概介紹下業(yè)務(wù)需求:從用戶登錄到注銷這段時(shí)間內(nèi),收到后臺(tái)拍照的指令后完成拍照、保存、上傳。以下會(huì)基于這個(gè)業(yè)務(wù)場景來詳細(xì)介紹各步驟的實(shí)現(xiàn)。
1.初始化拍照的預(yù)覽界面
在測試的過程中發(fā)現(xiàn),拍照的預(yù)覽界面需要在可顯示的情況下生成,才能正常拍照,假如是直接創(chuàng)建SurfaceView實(shí)例作為預(yù)覽界面,然后直接調(diào)用拍照時(shí)會(huì)拋出native層的異常:take_failed。想過看源碼尋找問題的原因,發(fā)現(xiàn)相機(jī)核心的功能代碼都在native層上面,所以暫且放下,假定的認(rèn)為該在拍照時(shí)該預(yù)覽界面一定得在最上面一層顯示。
由于應(yīng)用不管是在前臺(tái)還是按home回到桌面,都需要滿足該條件,那這個(gè)預(yù)覽界面應(yīng)該是全局的,很容易的聯(lián)想到使用一個(gè)全局窗口來作為預(yù)覽界面的載體。這個(gè)全局窗口要是不可見的,不影響后面的界面正常交互。所以,就想到用全局的context來獲取WindowManager對象管理這個(gè)全局窗口。接下來直接看代碼:
package com.yuexunit.zjjk.service; import com.yuexunit.zjjk.util.Logger; import android.content.Context; import android.view.SurfaceView; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; /** * 隱藏的全局窗口,用于后臺(tái)拍照 * * @author WuRS */ public class CameraWindow { private static final String TAG = CameraWindow.class.getSimpleName(); private static WindowManager windowManager; private static Context applicationContext; private static SurfaceView dummyCameraView; /** * 顯示全局窗口 * * @param context */ public static void show(Context context) { if (applicationContext == null) { applicationContext = context.getApplicationContext(); windowManager = (WindowManager) applicationContext .getSystemService(Context.WINDOW_SERVICE); dummyCameraView = new SurfaceView(applicationContext); LayoutParams params = new LayoutParams(); params.width = 1; params.height = 1; params.alpha = 0; params.type = LayoutParams.TYPE_SYSTEM_ALERT; // 屏蔽點(diǎn)擊事件 params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCHABLE; windowManager.addView(dummyCameraView, params); Logger.d(TAG, TAG + " showing"); } } /** * @return 獲取窗口視圖 */ public static SurfaceView getDummyCameraView() { return dummyCameraView; } /** * 隱藏窗口 */ public static void dismiss() { try { if (windowManager != null && dummyCameraView != null) { windowManager.removeView(dummyCameraView); Logger.d(TAG, TAG + " dismissed"); } } catch (Exception e) { e.printStackTrace(); } } }
代碼很簡單,主要功能就是顯示這個(gè)窗口、獲取用于預(yù)覽的SurfaceView以及關(guān)閉窗口。
在這個(gè)業(yè)務(wù)中,show方法可以直接在自定義的Application類中調(diào)用。這樣,在應(yīng)用啟動(dòng)后,窗口就在了,只有在應(yīng)用銷毀(注意,結(jié)束所有Activity不會(huì)關(guān)閉,因?yàn)樗跏蓟贏pplication中,它的生命周期就為應(yīng)用級的,除非主動(dòng)調(diào)用dismiss方法主動(dòng)關(guān)閉)。
完成了預(yù)覽界面的初始化,整個(gè)實(shí)現(xiàn)其實(shí)已經(jīng)非常簡單了??赡茉S多人遇到的問題就是卡在沒有預(yù)覽界面該如何拍照這里,希望這樣一種取巧的方式可以幫助大家在以后的項(xiàng)目中遇到無法直接解決問題時(shí),可以考慮從另外的角度切入去解決問題。
2.完成Service拍照功能
這里將對上面的后續(xù)步驟進(jìn)行合并。先上代碼:
package com.yuexunit.zjjk.service; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.app.Service; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.hardware.Camera; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.PictureCallback; import android.os.IBinder; import android.os.Message; import android.text.TextUtils; import android.view.SurfaceView; import com.yuexunit.sortnetwork.android4task.UiHandler; import com.yuexunit.sortnetwork.task.TaskStatus; import com.yuexunit.zjjk.network.RequestHttp; import com.yuexunit.zjjk.util.FilePathUtil; import com.yuexunit.zjjk.util.ImageCompressUtil; import com.yuexunit.zjjk.util.Logger; import com.yuexunit.zjjk.util.WakeLockManager; /** * 后臺(tái)拍照服務(wù),配合全局窗口使用 * * @author WuRS */ public class CameraService extends Service implements PictureCallback { private static final String TAG = CameraService.class.getSimpleName(); private Camera mCamera; private boolean isRunning; // 是否已在監(jiān)控拍照 private String commandId; // 指令I(lǐng)D @Override public void onCreate() { Logger.d(TAG, "onCreate..."); super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { WakeLockManager.acquire(this); Logger.d(TAG, "onStartCommand..."); startTakePic(intent); return START_NOT_STICKY; } private void startTakePic(Intent intent) { if (!isRunning) { commandId = intent.getStringExtra("commandId"); SurfaceView preview = CameraWindow.getDummyCameraView(); if (!TextUtils.isEmpty(commandId) && preview != null) { autoTakePic(preview); } else { stopSelf(); } } } private void autoTakePic(SurfaceView preview) { Logger.d(TAG, "autoTakePic..."); isRunning = true; mCamera = getFacingFrontCamera(); if (mCamera == null) { Logger.w(TAG, "getFacingFrontCamera return null"); stopSelf(); return; } try { mCamera.setPreviewDisplay(preview.getHolder()); mCamera.startPreview();// 開始預(yù)覽 // 防止某些手機(jī)拍攝的照片亮度不夠 Thread.sleep(200); takePicture(); } catch (Exception e) { e.printStackTrace(); releaseCamera(); stopSelf(); } } private void takePicture() throws Exception { Logger.d(TAG, "takePicture..."); try { mCamera.takePicture(null, null, this); } catch (Exception e) { Logger.d(TAG, "takePicture failed!"); e.printStackTrace(); throw e; } } private Camera getFacingFrontCamera() { CameraInfo cameraInfo = new CameraInfo(); int numberOfCameras = Camera.getNumberOfCameras(); for (int i = 0; i < numberOfCameras; i++) { Camera.getCameraInfo(i, cameraInfo); if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) { try { return Camera.open(i); } catch (Exception e) { e.printStackTrace(); } } } return null; } @Override public void onPictureTaken(byte[] data, Camera camera) { Logger.d(TAG, "onPictureTaken..."); releaseCamera(); try { // 大于500K,壓縮預(yù)防內(nèi)存溢出 Options opts = null; if (data.length > 500 * 1024) { opts = new Options(); opts.inSampleSize = 2; } Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, opts); // 旋轉(zhuǎn)270度 Bitmap newBitmap = ImageCompressUtil.rotateBitmap(bitmap, 270); // 保存 String fullFileName = FilePathUtil.getMonitorPicPath() + System.currentTimeMillis() + ".jpeg"; File saveFile = ImageCompressUtil.convertBmpToFile(newBitmap, fullFileName); ImageCompressUtil.recyleBitmap(newBitmap); if (saveFile != null) { // 上傳 RequestHttp.uploadMonitorPic(callbackHandler, commandId, saveFile); } else { // 保存失敗,關(guān)閉 stopSelf(); } } catch (Exception e) { e.printStackTrace(); stopSelf(); } } private UiHandler callbackHandler = new UiHandler() { @Override public void receiverMessage(Message msg) { switch (msg.arg1) { case TaskStatus.LISTENNERTIMEOUT: case TaskStatus.ERROR: case TaskStatus.FINISHED: // 請求結(jié)束,關(guān)閉服務(wù) stopSelf(); break; } } }; // 保存照片 private boolean savePic(byte[] data, File savefile) { FileOutputStream fos = null; try { fos = new FileOutputStream(savefile); fos.write(data); fos.flush(); fos.close(); return true; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } return false; } private void releaseCamera() { if (mCamera != null) { Logger.d(TAG, "releaseCamera..."); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } @Override public void onDestroy() { super.onDestroy(); Logger.d(TAG, "onDestroy..."); commandId = null; isRunning = false; FilePathUtil.deleteMonitorUploadFiles(); releaseCamera(); WakeLockManager.release(); } @Override public IBinder onBind(Intent intent) { return null; } }
代碼也不多,不過有幾個(gè)點(diǎn)需要特別注意下,
1.相機(jī)在通話時(shí)是用不了的,或者別的應(yīng)用持有該相機(jī)時(shí)也是獲取不到相機(jī)的,所以需要捕獲camera.Open()的異常,防止獲取不到相機(jī)時(shí)應(yīng)用出錯(cuò);
2.在用華為相機(jī)測試時(shí),開始預(yù)覽立馬拍照,發(fā)現(xiàn)獲取的照片亮度很低,原因只是猜測,具體需要去查資料。所以暫且的解決方案是讓線程休眠200ms,然后再調(diào)用拍照。
3.在不使用Camera資源或者發(fā)生任何異常時(shí),請記得釋放Camera資源,否則為導(dǎo)致相機(jī)被一直持有,別的應(yīng)用包括系統(tǒng)的相機(jī)也用不了,只能重啟手機(jī)解決。代碼大家可以優(yōu)化下, 把非正常業(yè)務(wù)邏輯統(tǒng)一處理掉?;蛘呤?,使用自定義的UncaughtExceptionHandler去處理未捕獲的異常。
4.關(guān)于代碼中WakeLocaManager類,是我自己封裝的喚醒鎖管理類,這也是大家在處理后臺(tái)關(guān)鍵業(yè)務(wù)時(shí)需要特別關(guān)注的一點(diǎn),保證業(yè)務(wù)邏輯在處理時(shí),系統(tǒng)不會(huì)進(jìn)入休眠。等業(yè)務(wù)邏輯處理完,釋放喚醒鎖,讓系統(tǒng)進(jìn)入休眠。
三、總結(jié)
該方案問題也比較多,只是提供一種思路。全局窗口才是這個(gè)方案的核心。相機(jī)的操作需要謹(jǐn)慎,獲取的時(shí)候需要捕獲異常(native異常,連接相機(jī)錯(cuò)誤,相信大家也遇到過),不使用或異常時(shí)及時(shí)釋放(可以把相機(jī)對象寫成static,然后在全局的異常捕獲中對相機(jī)做釋放,防止在持有相機(jī)這段時(shí)間內(nèi)應(yīng)用異常時(shí)導(dǎo)致相機(jī)被異常持有),不然別的相機(jī)應(yīng)用使用不了。
代碼大家稍作修改就可以使用,記得添加相關(guān)的權(quán)限。以下是系統(tǒng)窗口、喚醒鎖、相機(jī)的權(quán)限。如果用到自動(dòng)對焦再拍照,記得聲明以下uses-feature標(biāo)簽。其它常用權(quán)限這里就不贅述。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.CAMERA" />
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android中自定義控件的declare-styleable屬性重用方案
這篇文章主要介紹了Android中自定義控件的declare-styleable屬性重用方案,本文給出了一個(gè)終極重用解決方案,需要的朋友可以參考下2015-01-01SurfaceView播放視頻發(fā)送彈幕并實(shí)現(xiàn)滾動(dòng)歌詞
這篇文章主要為大家詳細(xì)介紹了SurfaceView播放視頻發(fā)送彈幕并實(shí)現(xiàn)滾動(dòng)歌詞,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11android 獲取APP的唯一標(biāo)識applicationId的實(shí)例
下面小編就為大家分享一篇android 獲取APP的唯一標(biāo)識applicationId的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02Android自定義View實(shí)現(xiàn)水波紋引導(dǎo)動(dòng)畫
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)水波紋動(dòng)畫引導(dǎo),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01Android RecyclerView網(wǎng)格布局示例解析
這篇文章主要介紹了Android RecyclerView網(wǎng)格布局示例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12Android優(yōu)質(zhì)索尼滾動(dòng)相冊
這篇文章主要介紹了Android優(yōu)質(zhì)索尼滾動(dòng)相冊,桌面小部件滾動(dòng)相冊,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09Android ListView實(shí)現(xiàn)下拉加載功能
這篇文章主要為大家詳細(xì)介紹了Android ListView實(shí)現(xiàn)下拉加載功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Android開發(fā)之Parcel機(jī)制實(shí)例分析
這篇文章主要介紹了Android開發(fā)之Parcel機(jī)制,實(shí)例分析了Parcel機(jī)制的原理與實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-05-05