Android 使用 CameraX 實(shí)現(xiàn)拍照和錄制視頻
原文鏈接: https://juejin.cn/post/6862180757314469901
AndroidX是Jetpack包下的組件,谷歌幫你考慮好了很多細(xì)節(jié),用就完事了。這些細(xì)節(jié)想自己設(shè)置的話也可以,不設(shè)置使用默認(rèn)值照樣很舒服。
導(dǎo)包
implementation?"androidx.camera:camera-camera2:1.0.0-beta07"
implementation?"androidx.camera:camera-view:1.0.0-alpha14"
implementation?"androidx.camera:camera-extensions:1.0.0-alpha14"
implementation?"androidx.camera:camera-lifecycle:1.0.0-beta07"
請(qǐng)求權(quán)限
在 Manifest 的 manifest 節(jié)點(diǎn)下中加入以下內(nèi)容:
<uses-permission?android:name="android.permission.CAMERA"?/>
<uses-feature?android:name="android.hardware.camera.any"?/>
<uses-permission?android:name="android.permission.WRITE_EXTERNAL_STORAGE"?/>
<uses-permission?android:name="android.permission.READ_EXTERNAL_STORAGE"?/>
<uses-permission?android:name="android.permission.RECORD_AUDIO"?/>
Android Q適配
在 manifest 標(biāo)簽里面加入一條屬性:
?android:requestLegacyExternalStorage="true"
畫黃線不理,如果不加這句,在Android Q上會(huì)無法往相冊(cè)存儲(chǔ)文件。
創(chuàng)建預(yù)覽布局
這里參考官方Demo的寫法,最底部圖層是一個(gè)PreviewView用來預(yù)覽,上層放兩個(gè)按鈕,一個(gè)用來拍照,一個(gè)用來錄像。
<androidx.constraintlayout.widget.ConstraintLayout?xmlns:android="http://schemas.android.com/apk/res/android"
????xmlns:app="http://schemas.android.com/apk/res-auto"
????xmlns:tools="http://schemas.android.com/tools"
????android:layout_width="match_parent"
????android:layout_height="match_parent"
????tools:context=".MainActivity">
????<Button
????????android:id="@+id/btnStartVideo"
????????android:layout_width="100dp"
????????android:layout_height="100dp"
????????android:layout_marginBottom="50dp"
????????android:elevation="2dp"
????????android:scaleType="fitCenter"
????????android:text="Start?Video"
????????app:layout_constraintBottom_toBottomOf="parent"
????????app:layout_constraintEnd_toEndOf="parent"
????????app:layout_constraintStart_toEndOf="@id/camera_capture_button"?/>
????<Button
????????android:id="@+id/camera_capture_button"
????????android:layout_width="100dp"
????????android:layout_height="100dp"
????????android:elevation="2dp"
????????android:scaleType="fitCenter"
????????android:text="Take?Photo"
????????app:layout_constraintBottom_toBottomOf="@+id/btnStartVideo"
????????app:layout_constraintEnd_toStartOf="@id/btnStartVideo"
????????app:layout_constraintStart_toStartOf="parent"?/>
????<androidx.camera.view.PreviewView
????????android:id="@+id/viewFinder"
????????android:layout_width="match_parent"
????????android:layout_height="match_parent"?/>
androidx.constraintlayout.widget.ConstraintLayout>
Activity中申請(qǐng)權(quán)限
聲明權(quán)限列表
companion?object?{
????private?const?val?REQUEST_CODE_PERMISSIONS?=?10
????private?val?REQUIRED_PERMISSIONS?=?arrayOf(Manifest.permission.CAMERA,?Manifest.permission.WRITE_EXTERNAL_STORAGE,
????????????????Manifest.permission.READ_EXTERNAL_STORAGE,?Manifest.permission.RECORD_AUDIO)
}
判斷當(dāng)前是否已有權(quán)限的方法
private?fun?allPermissionsGranted()?=?REQUIRED_PERMISSIONS.all?{
????ContextCompat.checkSelfPermission(baseContext,?it)?==?PackageManager.PERMISSION_GRANTED
}
在 onCreate 里開始主要邏輯。如果已有權(quán)限,開啟相機(jī)預(yù)覽。
?if?(allPermissionsGranted())?{
??????startCamera()
}?else?{
??????ActivityCompat.requestPermissions(this,?REQUIRED_PERMISSIONS,?REQUEST_CODE_PERMISSIONS)
}
在請(qǐng)求權(quán)限返回的時(shí)候,判斷是否已有權(quán)限,如果有了就可以開啟預(yù)覽了
override?fun?onRequestPermissionsResult(requestCode:?Int,?permissions:?Array<String>,?grantResults:?IntArray)?{
????if?(requestCode?==?REQUEST_CODE_PERMISSIONS)?{
????????if?(allPermissionsGranted())?{
????????????startCamera()
????????}?else?{
????????????Toast.makeText(this,?"Permissions?not?granted?by?the?user.",?Toast.LENGTH_SHORT).show()
????????????finish()
????????}
????}
}
聲明全局變量
private?lateinit?var?cameraExecutor:?ExecutorService
var?cameraProvider:?ProcessCameraProvider??=?null//相機(jī)信息
var?preview:?Preview??=?null//預(yù)覽對(duì)象
var?cameraSelector?=?CameraSelector.DEFAULT_BACK_CAMERA//當(dāng)前相機(jī)
var?camera:?Camera??=?null//相機(jī)對(duì)象
private?var?imageCapture:?ImageCapture??=?null//拍照用例
var?videoCapture:?VideoCapture??=?null//錄像用例
開啟相機(jī)預(yù)覽
開啟預(yù)覽,把預(yù)覽內(nèi)容放進(jìn) PreviewView 里。
????private?fun?startCamera()?{
????????cameraExecutor?=?Executors.newSingleThreadExecutor()
????????val?cameraProviderFuture?=?ProcessCameraProvider.getInstance(this)
????????cameraProviderFuture.addListener(Runnable?{
????????????cameraProvider?=?cameraProviderFuture.get()//獲取相機(jī)信息
????????????//預(yù)覽配置
????????????preview?=?Preview.Builder()
????????????????????.build()
????????????????????.also?{
????????????????????????it.setSurfaceProvider(viewFinder.createSurfaceProvider())
????????????????????}
????????????imageCapture?=?ImageCapture.Builder().build()//拍照用例配置
????????????val?imageAnalyzer?=?ImageAnalysis.Builder()
????????????????????.build()
????????????????????.also?{
????????????????????????it.setAnalyzer(cameraExecutor,?LuminosityAnalyzer?{?luma?->
????????????????????????????Log.d(TAG,?"Average?luminosity:?$luma")
????????????????????????})
????????????????????}
????????????cameraSelector?=?CameraSelector.DEFAULT_BACK_CAMERA//使用后置攝像頭
????????????videoCapture?=?VideoCapture.Builder()//錄像用例配置
//????????????????.setTargetAspectRatio(AspectRatio.RATIO_16_9)?//設(shè)置高寬比
//????????????????.setTargetRotation(viewFinder.display.rotation)//設(shè)置旋轉(zhuǎn)角度
//????????????????.setAudioRecordSource(AudioSource.MIC)//設(shè)置音頻源麥克風(fēng)
????????????????????.build()
????????????try?{
????????????????cameraProvider?.unbindAll()//先解綁所有用例
????????????????camera?=?cameraProvider?.bindToLifecycle(this,?cameraSelector,?preview,?imageCapture,?videoCapture)//綁定用例
????????????}?catch?(exc:?Exception)?{
????????????????Log.e(TAG,?"Use?case?binding?failed",?exc)
????????????}
????????},?ContextCompat.getMainExecutor(this))
????}
監(jiān)聽,寫在Activity外面
typealias?LumaListener?=?(luma:?Double)?->?Unit
再在Activity里面寫一個(gè)內(nèi)部類
private?class?LuminosityAnalyzer(private?val?listener:?LumaListener)?:?ImageAnalysis.Analyzer?{
????????private?fun?ByteBuffer.toByteArray():?ByteArray?{
????????????rewind()
????????????val?data?=?ByteArray(remaining())
????????????get(data)
????????????return?data
????????}
????????override?fun?analyze(image:?ImageProxy)?{
????????????val?buffer?=?image.planes[0].buffer
????????????val?data?=?buffer.toByteArray()
????????????val?pixels?=?data.map?{?it.toInt()?and?0xFF?}
????????????val?luma?=?pixels.average()
????????????listener(luma)
????????????image.close()
????????}
????}
界面銷毀時(shí)關(guān)閉線程
override?fun?onDestroy()?{
????super.onDestroy()
????cameraExecutor.shutdown()
}
拍照方法
private?fun?takePhoto()?{
????val?imageCapture?=?imageCapture??:?return
????val?file?=?File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).path?+
????????????"/CameraX"?+?SimpleDateFormat(FILENAME_FORMAT,?Locale.CHINA).format(System.currentTimeMillis())?+?".jpg")
????val?outputOptions?=?ImageCapture.OutputFileOptions.Builder(file).build()
????imageCapture.takePicture(outputOptions,?ContextCompat.getMainExecutor(this),
????????????object?:?ImageCapture.OnImageSavedCallback?{
????????????????override?fun?onError(exc:?ImageCaptureException)?{
????????????????????Log.e(TAG,?"Photo?capture?failed:?${exc.message}",?exc)
????????????????}
????????????????override?fun?onImageSaved(output:?ImageCapture.OutputFileResults)?{
????????????????????val?savedUri?=?Uri.fromFile(file)
????????????????????val?msg?=?"Photo?capture?succeeded:?$savedUri"
????????????????????Toast.makeText(baseContext,?msg,?Toast.LENGTH_SHORT).show()
????????????????????Log.d(TAG,?msg)
????????????????}
????????????})
}
錄像方法
private?fun?takeVideo()?{
????//視頻保存路徑
????val?file?=?File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).path?+?"/CameraX"?+?SimpleDateFormat(
????????????FILENAME_FORMAT,?Locale.CHINA
????).format(System.currentTimeMillis())?+?".mp4")
????//開始錄像
????videoCapture?.startRecording(file,?Executors.newSingleThreadExecutor(),?object?:?OnVideoSavedCallback?{
????????override?fun?onVideoSaved(@NonNull?file:?File)?{
????????????//保存視頻成功回調(diào),會(huì)在停止錄制時(shí)被調(diào)用
????????????Toast.makeText(this@MainActivity,?file.absolutePath,?Toast.LENGTH_SHORT).show()
????????}
????????override?fun?onError(videoCaptureError:?Int,?message:?String,?cause:?Throwable?)?{
????????????//保存失敗的回調(diào),可能在開始或結(jié)束錄制時(shí)被調(diào)用
????????????Log.e("",?"onError:?$message")
????????}
????})
????btnStartVideo.setOnClickListener?{
????????//結(jié)束錄像
????????videoCapture?.stopRecording()//停止錄制
????????preview?.clear()//清除預(yù)覽
????????btnStartVideo.text?=?"Start?Video"
????????btnStartVideo.setOnClickListener?{
????????????btnStartVideo.text?=?"Stop?Video"
????????????takeVideo()
????????}
????????Toast.makeText(this,?file.path,?Toast.LENGTH_SHORT).show()
????????Log.d("path",?file.path)
????}
}
文中代碼完整Demo
Github CameraX-Demo?https://github.com/DubheBroken/CameraX-Demo
參考文章:
Google CameraX 開發(fā)文檔 ?
掘金-JetPack之使用CameraX完成拍照和拍視頻 ?
Stackoverflow-Exception 'open failed: EACCES (Permission denied)' on Android
