前言
在上一篇文章中給小伙伴們介紹了進(jìn)行Camera開發(fā)需要了解的知識(shí)點(diǎn),如果你還沒(méi)有看過(guò)的話斋配,建議先去看上一篇文章《Android: Camera相機(jī)開發(fā)詳解(上) —— 知識(shí)儲(chǔ)備》
本篇文章會(huì)帶著小伙伴們一步一步實(shí)現(xiàn)自己的Camera孔飒,并在實(shí)現(xiàn)的過(guò)程中驗(yàn)證上一篇中所講解的結(jié)論
實(shí)現(xiàn)思路:
在xml布局中定義一個(gè)SurfaceView,用于預(yù)覽相機(jī)采集的數(shù)據(jù)
給SurfaceHolder添加回調(diào)艰争,在surfaceCreated(holder: SurfaceHolder?)回調(diào)中打開相機(jī)
成功打開相機(jī)后坏瞄,設(shè)置相機(jī)參數(shù)。比如:對(duì)焦模式甩卓,預(yù)覽大小鸠匀,照片保存大小等等
設(shè)置相機(jī)預(yù)覽時(shí)的旋轉(zhuǎn)角度,然后調(diào)用startPreview()開始預(yù)覽
調(diào)用takePicture方法拍照 或者 是在Camera的預(yù)覽回調(diào)中 保存照片
對(duì)保存的照片進(jìn)行旋轉(zhuǎn)處理逾柿,使其為"自然方向"
關(guān)閉頁(yè)面缀棍,釋放相機(jī)資源
具體實(shí)現(xiàn)步驟:
一丶申請(qǐng)權(quán)限
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
二、在xml布局文件中定義一個(gè)SurfaceView
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
三机错、創(chuàng)建一個(gè)CameraHelper類
class CameraHelper(activity: Activity, surfaceView: SurfaceView) : Camera.PreviewCallback {
private var mCamera: Camera? = null //Camera對(duì)象
private lateinit var mParameters: Camera.Parameters //Camera對(duì)象的參數(shù)
private var mSurfaceView: SurfaceView = surfaceView //用于預(yù)覽的SurfaceView對(duì)象
var mSurfaceHolder: SurfaceHolder //SurfaceHolder對(duì)象
private var mActivity: Activity = activity
private var mCallBack: CallBack? = null //自定義的回調(diào)
var mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK //攝像頭方向
var mDisplayOrientation: Int = 0 //預(yù)覽旋轉(zhuǎn)的角度
private var picWidth = 2160 //保存圖片的寬
private var picHeight = 3840 //保存圖片的高
}
由于對(duì)Camera的操作等代碼比較多爬范,本著各司其職的原則,創(chuàng)建了一個(gè)CameraHelper類來(lái)處理Camera相關(guān)的操作弱匪,如果放在Activity中對(duì)Camera操作會(huì)使Activity臃腫復(fù)雜
CameraHelper的構(gòu)造方法有兩個(gè)青瀑,一個(gè)是Activity對(duì)象,一個(gè)是SurfaceView對(duì)象(就是xml文件里定義的SurfaceView)
四萧诫、給SurfaceView對(duì)象添加回調(diào)函數(shù)斥难,并初始化相機(jī)
private fun init() {
mSurfaceHolder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
releaseCamera() //釋放相機(jī)資源
}
override fun surfaceCreated(holder: SurfaceHolder?) { //surface創(chuàng)建
if (mCamera == null) {
openCamera(mCameraFacing) //打開相機(jī)
}
startPreview() //開始預(yù)覽
}
})
}
//打開相機(jī)
private fun openCamera(cameraFacing: Int = Camera.CameraInfo.CAMERA_FACING_BACK): Boolean {
var supportCameraFacing = supportCameraFacing(cameraFacing) //判斷手機(jī)是否支持前置/后置攝像頭
if (supportCameraFacing) {
try {
mCamera = Camera.open(cameraFacing)
initParameters(mCamera!!) //初始化相機(jī)配置信息
mCamera?.setPreviewCallback(this)
} catch (e: Exception) {
e.printStackTrace()
toast("打開相機(jī)失敗!")
return false
}
}
return supportCameraFacing
}
//判斷是否支持某個(gè)相機(jī)
private fun supportCameraFacing(cameraFacing: Int): Boolean {
var info = Camera.CameraInfo()
for (i in 0 until Camera.getNumberOfCameras()) {
Camera.getCameraInfo(i, info)
if (info.facing == cameraFacing) return true
}
return false
}
在CameraHelper的創(chuàng)建后調(diào)用init()方法。在init()方法中财搁,我們首先對(duì)mSurfaceHolder添加了一個(gè)回調(diào)蘸炸,這個(gè)回調(diào)會(huì)告訴我們SurfaceView中surface的變化(在上一篇上有講解)
在surfaceCreated(holder: SurfaceHolder?) 回調(diào)中打開相機(jī)躬络。因?yàn)橄鄼C(jī)開始預(yù)覽的時(shí)候尖奔,如果SurfaceView中的surface還沒(méi)有創(chuàng)建,就回拋出異常,所以我們?cè)趕urface創(chuàng)建后再對(duì)相機(jī)進(jìn)行操作
我們調(diào)用相機(jī)的open()方法打開一個(gè)攝像頭提茁,在打開攝像頭之前判斷一下手機(jī)是否支持我們將要打開的攝像頭淹禾。
五、配置相機(jī)參數(shù)
//配置相機(jī)參數(shù)
private fun initParameters(camera: Camera) {
try {
mParameters = camera.parameters
mParameters.previewFormat = ImageFormat.NV21 //設(shè)置預(yù)覽圖片的格式
//獲取與指定寬高相等或最接近的尺寸
//設(shè)置預(yù)覽尺寸
val bestPreviewSize = getBestSize(mSurfaceView.width, mSurfaceView.height, mParameters.supportedPreviewSizes)
bestPreviewSize?.let {
mParameters.setPreviewSize(it.width, it.height)
}
//設(shè)置保存圖片尺寸
val bestPicSize = getBestSize(picWidth, picHeight, mParameters.supportedPictureSizes)
bestPicSize?.let {
mParameters.setPictureSize(it.width, it.height)
}
//對(duì)焦模式
if (isSupportFocus(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE))
mParameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
camera.parameters = mParameters
} catch (e: Exception) {
e.printStackTrace()
toast("相機(jī)初始化失敗!")
}
}
//獲取與指定寬高相等或最接近的尺寸
private fun getBestSize(targetWidth: Int, targetHeight: Int, sizeList: List<Camera.Size>): Camera.Size? {
var bestSize: Camera.Size? = null
var targetRatio = (targetHeight.toDouble() / targetWidth) //目標(biāo)大小的寬高比
var minDiff = targetRatio
for (size in sizeList) {
var supportedRatio = (size.width.toDouble() / size.height)
log("系統(tǒng)支持的尺寸 : ${size.width} * ${size.height} , 比例$supportedRatio")
}
for (size in sizeList) {
if (size.width == targetHeight && size.height == targetWidth) {
bestSize = size
break
}
var supportedRatio = (size.width.toDouble() / size.height)
if (Math.abs(supportedRatio - targetRatio) < minDiff) {
minDiff = Math.abs(supportedRatio - targetRatio)
bestSize = size
}
}
log("目標(biāo)尺寸 :$targetWidth * $targetHeight 茴扁, 比例 $targetRatio")
log("最優(yōu)尺寸 :${bestSize?.height} * ${bestSize?.width}")
return bestSize
}
我們對(duì)預(yù)覽大小和保存圖片大小進(jìn)行設(shè)置铃岔,在設(shè)置的時(shí)候,我們應(yīng)該獲取到與指定寬高相等或最接近的尺寸峭火,這樣的話才能保證圖片既不變形又能最接近我們指定的大小毁习。
下面是vivo x9的后置攝像頭支持的尺寸:
六、開始預(yù)覽
//開始預(yù)覽
fun startPreview() {
mCamera?.let {
it.setPreviewDisplay(mSurfaceHolder) //設(shè)置相機(jī)預(yù)覽對(duì)象
// setCameraDisplayOrientation(mActivity) //設(shè)置預(yù)覽時(shí)相機(jī)旋轉(zhuǎn)的角度
it.startPreview()
}
}
調(diào)用startPreview()方法開始預(yù)覽卖丸,我們先看一下預(yù)覽效果:
我們可以看到纺且,畫面并不是"自然方向"而且被拉伸。這個(gè)在上一篇已經(jīng)講解過(guò)稍浆,下面通過(guò)setDisplayOrientation(int degree)方法载碌,使其正常顯示
//設(shè)置預(yù)覽旋轉(zhuǎn)的角度
private fun setCameraDisplayOrientation(activity: Activity) {
var info = Camera.CameraInfo()
Camera.getCameraInfo(mCameraFacing, info)
val rotation = activity.windowManager.defaultDisplay.rotation
var screenDegree = 0
when (rotation) {
Surface.ROTATION_0 -> screenDegree = 0
Surface.ROTATION_90 -> screenDegree = 90
Surface.ROTATION_180 -> screenDegree = 180
Surface.ROTATION_270 -> screenDegree = 270
}
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
mDisplayOrientation = (info.orientation + screenDegree) % 360
mDisplayOrientation = (360 - mDisplayOrientation) % 360 // compensate the mirror
} else {
mDisplayOrientation = (info.orientation - screenDegree + 360) % 360
}
mCamera?.setDisplayOrientation(mDisplayOrientation)
log("屏幕的旋轉(zhuǎn)角度 : $rotation")
log("setDisplayOrientation(result) : $mDisplayOrientation")
}
設(shè)置后預(yù)覽效果如下:
上一篇提到的相機(jī)的預(yù)覽方向:
通過(guò)日志我們看到,前后攝像頭的預(yù)覽旋轉(zhuǎn)角度都是90
前置攝像頭在進(jìn)行角度旋轉(zhuǎn)之前衅枫,圖像會(huì)進(jìn)行一個(gè)水平的鏡像翻轉(zhuǎn)嫁艇,所以前置攝像頭應(yīng)該設(shè)置的旋轉(zhuǎn)角度是 270 - 180 = 90
七、進(jìn)行拍照
拍照的話有兩種方式:
調(diào)用takePicture(ShutterCallback shutter, PictureCallback raw,
PictureCallback jpeg) 方法在相機(jī)的預(yù)覽回調(diào)里直接保存
1.調(diào)用takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 拍照
//拍攝照片
fun takePic() {
mCamera?.let {
it.takePicture({}, null, { data, _ ->
it.startPreview()
savePic(data) //保存圖片
})
}
}
//保存照片
private fun savePic(data: ByteArray?) {
thread {
try {
val temp = System.currentTimeMillis()
val picFile = FileUtil.createCameraFile()
if (picFile != null && data != null) {
val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close()
runOnUiThread {
toast("圖片已保存! ${picFile.absolutePath}")
log("圖片已保存! 耗時(shí):${System.currentTimeMillis() - temp} 路徑: ${picFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
runOnUiThread {
toast("保存圖片失斚伊谩步咪!")
}
}
}
}
takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg)方法有3個(gè)參數(shù),而且這3個(gè)參數(shù)都是抽象接口:
第一個(gè)是點(diǎn)擊拍照時(shí)的回調(diào)益楼。
如果傳null歧斟,則沒(méi)有任何效果
如果寫一個(gè)空實(shí)現(xiàn),則在點(diǎn)擊拍照時(shí)會(huì)有"咔擦"聲第二個(gè)和第三個(gè)參數(shù)類型一樣偏形,PictureCallback 有一個(gè)抽象方法
void onPictureTaken(byte[] data, Camera camera)
data就是點(diǎn)擊拍照后相機(jī)返回的照片的byte數(shù)組静袖,用該數(shù)組創(chuàng)建一個(gè)bitmap保存下來(lái),就得到了拍攝的照片
2.在相機(jī)的預(yù)覽回調(diào)里直接保存
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
savePic(data) //保存照片
}
注意:實(shí)際上這個(gè)回調(diào)方法會(huì)一直一直的調(diào)用俊扭,如果要保存一張照片的話應(yīng)該加個(gè)字段進(jìn)行控制队橙,此處只是做演示
在保存圖片的時(shí)候,我們需要開啟一個(gè)子線程來(lái)進(jìn)行操作萨惑,通過(guò)日志輸出可以看到保存圖片所用時(shí)間和保存路徑:
八捐康、調(diào)整保存照片的方向
與預(yù)覽時(shí)方向類似,照片在保存時(shí)也有一個(gè)方向庸蔼。我們先看一下在上一步中保存的照片是什么樣的:
后置攝像頭:
前置攝像頭:
下面我們?cè)诒4鎴D片的時(shí)候解总,對(duì)照片進(jìn)行旋轉(zhuǎn)處理,保存照片的方法應(yīng)該如下:
private fun savePic(data: ByteArray?) {
thread {
try {
val temp = System.currentTimeMillis()
val picFile = FileUtil.createCameraFile()
if (picFile != null && data != null) {
val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
val resultBitmap = if (mCameraHelper.mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT)
BitmapUtils.rotate(rawBitmap, 270f) //前置攝像頭旋轉(zhuǎn)270°
else
BitmapUtils.rotate(rawBitmap, 90f) //后置攝像頭旋轉(zhuǎn)90°
Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close()
runOnUiThread {
toast("圖片已保存! ${picFile.absolutePath}")
log("圖片已保存! 耗時(shí):${System.currentTimeMillis() - temp} 路徑: ${picFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
runOnUiThread {
toast("保存圖片失斀憬觥花枫!")
}
}
}
}
//圖片工具類
object BitmapUtils {
//水平鏡像翻轉(zhuǎn)
fun mirror(rawBitmap: Bitmap): Bitmap {
var matrix = Matrix()
matrix.postScale(-1f, 1f)
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
}
//旋轉(zhuǎn)
fun rotate(rawBitmap: Bitmap, degree: Float): Bitmap {
var matrix = Matrix()
matrix.postRotate(degree)
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
}
|
然后我們?cè)谶M(jìn)行一次拍照:
后置攝像頭:
前置攝像頭:
對(duì)比一下上一篇文章所講的相機(jī)保存照片的方向:
關(guān)于前置攝像頭所拍攝照片,需要注意的是劳翰,由于在setDisplayOrientation()設(shè)置相機(jī)預(yù)覽方向的時(shí)候系統(tǒng)默認(rèn)做了一個(gè)水平鏡面的翻轉(zhuǎn)敦锌,所以我們通過(guò)前置攝像頭保存來(lái)的照片并不是和預(yù)覽時(shí)看到的一樣,兩者是水平鏡像關(guān)系佳簸。所以乙墙,一般情況下我們不僅僅需要對(duì)前置攝像頭做旋轉(zhuǎn),還應(yīng)該做一個(gè)水平方向的鏡面翻轉(zhuǎn)處理生均。
在上面保存圖片的方法中判斷如果是前置攝像頭的話听想,代碼修改如下:
BitmapUtils.mirror(BitmapUtils.rotate(rawBitmap, 270f)) //旋轉(zhuǎn)270,然后水平鏡面翻轉(zhuǎn)
這樣的話马胧,就能保證所拍攝照片與在預(yù)覽時(shí)所呈現(xiàn)的畫面是一模一樣的哗魂,如下圖:
注:如果有小伙伴對(duì)這點(diǎn)還不太理解的話,墻裂建議自己用前置攝像頭自拍一張漓雅,然后在對(duì)比保存的照片與預(yù)覽時(shí)手機(jī)里顯示的畫面录别,就很容易理解了
不是我不愿意自己自拍來(lái)給小伙們演示,長(zhǎng)相實(shí)在是有點(diǎn)慘邻吞,所以大家還是自己親自驗(yàn)證吧o(╥﹏╥)o
九组题、釋放相機(jī)資源
在Activity銷毀前或者是關(guān)閉相機(jī)時(shí),應(yīng)當(dāng)釋放當(dāng)前相機(jī)資源
//釋放相機(jī)
fun releaseCamera() {
if (mCamera != null) {
mCamera?.stopPreview()
mCamera?.setPreviewCallback(null)
mCamera?.release()
mCamera = null
}
}
完整效果如下:
總結(jié)
本篇文章主要給小伙伴們介紹了實(shí)現(xiàn)Camera拍照功能的流程及步驟抱冷,并且用實(shí)際效果驗(yàn)證了上一篇文章中所講解的理論
下一篇文章將會(huì)給小伙伴們介紹如何實(shí)現(xiàn)人臉檢測(cè)功能崔列,敬請(qǐng)期待~~
完整代碼
https://github.com/smashinggit/Study
注:此工程包含多個(gè)module,本文所用代碼均在camerademo文件夾下