Android 系統(tǒng)原生相機(jī)API角度原理與適配

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ī)拍照的生命周期

Camera生命周期.png

更加準(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)容。

傳感器坐標(biāo)與view坐標(biāo)對(duì)比圖.png

左圖是通常情況下,我們對(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)該角度就能夠回正展示。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容