Camera1
雖然Camera作為第一代原生android所提供的相機(jī)類一直被開發(fā)者甚至Google官方開發(fā)人員所詬病,但為了兼容和適配Android版本5.0以下的App應(yīng)用,我們別無選擇。因此,有了本篇文檔詳細(xì)闡述1.0版的Camera 是如何使用的。本篇使用的是SurfaceView與Camera類。
一個(gè)方向,四個(gè)角度
- 終端自然方向
- 相機(jī)傳感器偏角
- 屏幕旋轉(zhuǎn)角度
- 終端自然方向偏角
- 圖像寫入偏角
文檔下文會(huì)在拍照流程中的不同的階段應(yīng)用到上述四個(gè)角度,而“終端自然方向”貫穿整個(gè)流程當(dāng)中。這一個(gè)方向、四個(gè)角度非常重要,缺一不可,是支撐相機(jī)Camera 系列API的關(guān)鍵。在設(shè)計(jì)NXDesign的相機(jī)項(xiàng)目中,經(jīng)過對(duì)官方文檔的研讀和各路資料的調(diào)研之后發(fā)現(xiàn),我們?cè)诰W(wǎng)絡(luò)上查到的博客類相關(guān)資料有80%的實(shí)現(xiàn)方式是存在問題的,當(dāng)然,這也可以歸咎于該API其本身確實(shí)不好用,如果不對(duì)源碼注釋進(jìn)行仔細(xì)研究,很容易對(duì)開發(fā)者產(chǎn)生誤導(dǎo)。
相機(jī)拍照的生命周期

更加準(zhǔn)確的說,相機(jī)的生命周期是依托于SurfaceView的創(chuàng)建和銷毀來完成的。SurfaceView的作用是提供相機(jī)內(nèi)容的實(shí)時(shí)預(yù)覽。我們需要在surfaceview創(chuàng)建好之后打開相機(jī)使用相機(jī)資源,在surfaceview被銷毀后釋放相機(jī)資源。
- 關(guān)聯(lián)surfaceview
surfaceview 提供了holder機(jī)制向調(diào)用方通知surfaceview的變化時(shí)機(jī),為了在不同的時(shí)機(jī)對(duì)相機(jī)資源做不同的事情,需要調(diào)用SurfaceHolder.addCallback()方法。
surfaceview.holder.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?) {
//開啟相機(jī)
startCamera(Camera.CameraInfo.CAMERA_FACING_BACK)
}
})
- 打開相機(jī),進(jìn)行預(yù)覽(終端自然方向、相機(jī)傳感器偏角)
現(xiàn)在的Android手機(jī)一般會(huì)有多個(gè)攝像頭,但根據(jù)其方向可以歸為兩類:CAMERA_FACING_BACK 和 CAMERA_FACING_FRONT。在打開攝像頭之前,首先需要獲取相機(jī)資源,判斷相機(jī)個(gè)數(shù)Camera.getNumberOfCameras()。每個(gè)相機(jī)對(duì)應(yīng)一個(gè)CameraInfo,它的定義如下:
public static class CameraInfo {
/**
* The facing of the camera is opposite to that of the screen.
* 前置攝像頭標(biāo)記
*/
public static final int CAMERA_FACING_BACK = 0;
/**
* The facing of the camera is the same as that of the screen.
* 后置攝像頭標(biāo)記
*/
public static final int CAMERA_FACING_FRONT = 1;
/**
* The direction that the camera faces. It should be
* CAMERA_FACING_BACK or CAMERA_FACING_FRONT.
* 攝像頭方向(值取CAMERA_FACING_BACK 或 CAMERA_FACING_FRONT)
*/
public int facing;
/**
* <p>The orientation of the camera image. The value is the angle that the
* camera image needs to be rotated clockwise so it shows correctly on
* the display in its natural orientation. It should be 0, 90, 180, or 270.</p>
*
* <p>For example, suppose a device has a naturally tall screen. The
* back-facing camera sensor is mounted in landscape. You are looking at
* the screen. If the top side of the camera sensor is aligned with the
* right edge of the screen in natural orientation, the value should be
* 90. If the top side of a front-facing camera sensor is aligned with
* the right of the screen, the value should be 270.</p>
*
* @see #setDisplayOrientation(int)
* @see Parameters#setRotation(int)
* @see Parameters#setPreviewSize(int, int)
* @see Parameters#setPictureSize(int, int)
* @see Parameters#setJpegThumbnailSize(int, int)
*
* 相機(jī)拍攝出來的圖片的旋轉(zhuǎn)角度。拍出的圖片需要順時(shí)針旋轉(zhuǎn)這個(gè)角度,才能正常展示。
* 取值只有0,90,180 和270 四種。
* 比如:一個(gè)手機(jī)是豎版屏幕,后置攝像頭的圖像傳感器是橫向物理擺放,當(dāng)你面向屏幕時(shí):
* 如果相機(jī)自帶的傳感器頂部與屏幕自然方向的右邊緣一致,則這個(gè)值就是90度。
* 如果前置攝像頭傳感器的頂部與手機(jī)自然方向一致,則這個(gè)值就是270度。
*/
public int orientation;
/**
* <p>Whether the shutter sound can be disabled.</p>
*
* <p>On some devices, the camera shutter sound cannot be turned off
* through {@link #enableShutterSound enableShutterSound}. This field
* can be used to determine whether a call to disable the shutter sound
* will succeed.</p>
*
* <p>If this field is set to true, then a call of
* {@code enableShutterSound(false)} will be successful. If set to
* false, then that call will fail, and the shutter sound will be played
* when {@link Camera#takePicture takePicture} is called.</p>
*/
public boolean canDisableShutterSound;
};
這里涉及到一個(gè)重要概念:相機(jī)圖像傳感器(camera sensor),想要理解上述注釋的含義,就需要先理解下圖內(nèi)容。

左圖是通常情況下,我們對(duì)view的x y方向的認(rèn)知,以屏幕的左上角為原點(diǎn)向右為x正方向,向下為y正方向;但是,右圖描述的是絕大多數(shù)情況下,相機(jī)圖像傳感器的起始位置和方向判定。與view不同的是,傳感器以手機(jī)屏幕在自然方向上的右上角為原點(diǎn),向下為x正方向,向左為y正方向。因此,我們理解上述注釋就不難了。如果相機(jī)自帶的傳感器頂部與終端自然方向(手機(jī)屏幕的硬件方向,一般手機(jī)都是豎直方向,也就是文檔中說的naturally tall screen)的右邊緣一致,則這個(gè)值就是90度。如果前置攝像頭傳感器的頂部與手機(jī)自然方向一致,則這個(gè)值就是270度。
當(dāng)我們定義startCamera()方法時(shí),要做5件事情,1.遍歷攝像頭cameraId,找到想要打開的攝像頭(前置還是后置);2.獲取攝像頭信息,主要獲取orientation;3. 設(shè)置相機(jī)DisplayOrientation 4.設(shè)置相機(jī)參數(shù),主要是寬高比、對(duì)焦模式、圖片格式、setRotation等。5. 向camera設(shè)置surfaceview.viewholder,并且startPreview。主要邏輯如下:
private fun startCamera(cameraFacing: Int) {
val numbers = Camera.getNumberOfCameras()
var targetCameraInfo: Camera.CameraInfo? = null
var targetId: Int? = null
for (i in 0 until numbers) {//1、遍歷攝像頭信息,找到需要的攝像頭
val cameraInfo = Camera.CameraInfo()
Camera.getCameraInfo(i, cameraInfo)
if (cameraInfo.facing == cameraFacing) {
targetCameraInfo = cameraInfo//2、獲取該攝像頭信息
targetId = i//獲取該攝像頭id,其id與下標(biāo)一致
break
}
}
if (targetCameraInfo != null && targetId != null) {
try {
curCameraDetail.cameraId = targetId
curCameraDetail.camera = Camera.open(targetId)
curCameraDetail.cameraFacing = cameraFacing
curCameraDetail.cameradetail = targetCameraInfo
setCameraDisplayOrientation(curCameraDetail)//3、設(shè)置surfaceview預(yù)覽方向
setParameters(curCameraDetail)//4、設(shè)置參數(shù)信息
startPreview(curCameraDetail.camera, surfaceview.holder)//5、開始預(yù)覽
} catch (e: RuntimeException) {
Toast.makeText(activity, "打開相機(jī)失敗,請(qǐng)檢查相機(jī)權(quán)限", Toast.LENGTH_SHORT).show()
pop()
}
} else {
//TODO:不支持
}
}
- 接下來,設(shè)置預(yù)覽方向
setCameraDisplayOrientation(curCameraDetail)
拿到cameraInfo.orientation之后,要調(diào)用camera.setDisplayOrientation設(shè)置進(jìn)去,保證通過surfaceview預(yù)覽到的取景跟當(dāng)前的手機(jī)方向保持一致,但是,setDisplayOrientation設(shè)置的其實(shí)是經(jīng)過兩個(gè)角度計(jì)算之后的復(fù)合角度,而并不單純是cameraInfo.orientation。正確的做法是這樣的:先獲取手機(jī)屏幕的旋轉(zhuǎn)方向,然后與cameraInfo.orientation加和得到最終角度。通常情況下,如果我們?cè)O(shè)置相機(jī)為portrait,則不用考慮rotation。這也是為什么絕大部分網(wǎng)絡(luò)資料中都會(huì)粗暴的寫入一個(gè)90度完事兒而并沒有解釋這么做的道理。
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.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;
}
camera.setDisplayOrientation(result);
}
- 設(shè)置參數(shù)信息
camera.parameters.setRotation(rotation)
setRotation()的意義是,設(shè)置一個(gè)需要順時(shí)針旋轉(zhuǎn)的角度,這個(gè)角度在拍照前以參數(shù)的形式設(shè)置給相機(jī),在拍照時(shí)會(huì)被寫入到拍照之后所保存的圖像文件中,并且可以通過Exif工具類拿到。之所以這么做是因?yàn)椋鄼C(jī)驅(qū)動(dòng)往往在生成照片的時(shí)候不會(huì)按照當(dāng)前的屏幕方向做出糾正,而是直接生成圖片,這就需要我們通過計(jì)算當(dāng)前的手機(jī)相對(duì)于自然方向(上文已解釋)的偏角以及相機(jī)傳感器偏角(上文已解釋)進(jìn)行計(jì)算,得到該角度。只要對(duì)原始圖片在順時(shí)針方向旋轉(zhuǎn)該角度之后,無論屏幕旋轉(zhuǎn)方向怎樣,都會(huì)在重力感應(yīng)的方向進(jìn)行正向的展示。
為了獲得這個(gè)角度,需要使用系統(tǒng)提供的android.view.OrientationEventListener:
public void onOrientationChanged(int orientation) {
if (orientation == ORIENTATION_UNKNOWN) return;
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
orientation = (orientation + 45) / 90 * 90;
int rotation = 0;
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else { // back-facing camera
rotation = (info.orientation + orientation) % 360;
}
mParameters.setRotation(rotation);
}
- 拍照
調(diào)用camera.takePicture(null, null, pictureCallback)
private val pictureCallback = Camera.PictureCallback { data, camera ->
val filePath = "${getExternalFileDir()}/original_${System.currentTimeMillis()}.jpg"
with(HandlerThread("background")) {
this.start()
Handler(looper)
}.post {
val originalFile = File(filePath)
FileOutputStream(originalFile).apply {
write(data)
close()
}
val file2 = toolCompressAndRotate(originalFile.absolutePath, getExternalFileDir(), "compress_${System.currentTimeMillis()}.jpg", 800, 600)
?: return@post
}
}
這里需要做的僅僅是將callback中返回的data存儲(chǔ)為File。需要注意的是,data中會(huì)包含setRotation()方法中的角度信息,因此如果直接使用Bitmap工具類生成bitmap,再進(jìn)行存儲(chǔ)或者展示,生成出來的圖像其實(shí)是缺失了旋轉(zhuǎn)角度的原始方向,這十有八九會(huì)發(fā)生圖像展示角度錯(cuò)誤的情況。因此,需要直接保存,再通過Exif工具類讀取File中的角度信息(當(dāng)然Exif工具類就是為了讀取File中的各種信息而生的,比如拍照時(shí)間、經(jīng)緯度等等)。
總結(jié)
基于Camera API,
surfaceview的預(yù)覽需要setDisplayOrientation(),入?yún)⒔嵌扰cCameraInfo.orientation(傳感器偏角)和WindowManager.default.displayOrientation(屏幕旋轉(zhuǎn)角度)兩個(gè)角度有關(guān)。
相機(jī)拍照前需要setRotation(),入?yún)⒔嵌扰cCameraInfo.orientation(傳感器偏角)和OrientationEventListener返回的orientation(終端自然角度偏角)有關(guān),二者的換算結(jié)果就是圖像寫入偏角,該偏角意味著圖像被順時(shí)針旋轉(zhuǎn)該角度就能夠回正展示。