控制相機

在本課程中,我們將討論如何使用架構 API 來直接控制相機硬體。

注意:本頁面所述是指已淘汰的相機類別。建議使用 CameraX,或者在特定使用案例下,使用 Camera2。CameraX 和 Camera2 均支援 Android 5.0 (API 級別 21) 以上版本。

比起從現有的相機應用程式要求圖片或影片,直接控制裝置相機的程式碼要多上許多。不過,如果您想要建構專用的相機應用程式,或是完整整合至應用程式的 UI,本課程介紹可以如何做到。

請參閱下列相關資源:

開啟相機物件

如要直接控制相機,首先要取得 Camera 物件的執行個體。如同 Android 本身相機應用程式的做法,存取相機的建議作法是,在透過 onCreate() 啟動的另一個單獨的執行緒中開啟 Camera。這是一個不錯的做法,因為可能需要一些時間才能完成,並且可能會使 UI 執行緒停滯。在更基本的實作中,開啟相機可轉由 onResume() 方法進行,可達到重複使用程式碼的效果,並簡化控制流程。

呼叫 Camera.open() 時,如果其他應用程式正在使用相機,系統會擲回例外狀況,因此會將該物件納入 try 區塊。

Kotlin

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        mCamera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(getString(R.string.app_name), "failed to open Camera")
        e.printStackTrace()
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    mCamera?.also { camera ->
        camera.release()
        mCamera = null
    }
}

Java

private boolean safeCameraOpen(int id) {
    boolean qOpened = false;

    try {
        releaseCameraAndPreview();
        camera = Camera.open(id);
        qOpened = (camera != null);
    } catch (Exception e) {
        Log.e(getString(R.string.app_name), "failed to open Camera");
        e.printStackTrace();
    }

    return qOpened;
}

private void releaseCameraAndPreview() {
    preview.setCamera(null);
    if (camera != null) {
        camera.release();
        camera = null;
    }
}

自 API 級別 9 開始,相機架構可支援多部相機。如果使用舊版 API,並且在沒有引數的情況下呼叫 open(),則會先取用背面相機。

建立相機預覽

拍照時,使用者通常需要看到拍攝主體的預覽畫面,才會按下快門。方法是使用 SurfaceView 來繪製相機感應器擷取的預覽畫面。

預覽類別

如要開始顯示預覽畫面,必須具備預覽類別。預覽時必須實作 android.view.SurfaceHolder.Callback 介面,用於將圖片資料從相機硬體傳送至應用程式。

Kotlin

class Preview(
        context: Context,
        val surfaceView: SurfaceView = SurfaceView(context)
) : ViewGroup(context), SurfaceHolder.Callback {

    var mHolder: SurfaceHolder = surfaceView.holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }
    ...
}

Java

class Preview extends ViewGroup implements SurfaceHolder.Callback {

    SurfaceView surfaceView;
    SurfaceHolder holder;

    Preview(Context context) {
        super(context);

        surfaceView = new SurfaceView(context);
        addView(surfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        holder = surfaceView.getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
...
}

預覽類別必須傳遞至 Camera 物件,才能開始進行即時圖片預覽,方式如下一節所示。

設定並開始預覽

必須以特定順序建立相機執行個體及其相關預覽,並優先顯示相機物件。下列程式碼片段封裝了相機的初始化程序,如此一來,每當使用者變更相機時,setCamera() 方法都會呼叫 Camera.startPreview()。另外也必須在預覽類別 surfaceChanged() 回呼方法中重新啟動預覽。

Kotlin

fun setCamera(camera: Camera?) {
    if (mCamera == camera) {
        return
    }

    stopPreviewAndFreeCamera()

    mCamera = camera

    mCamera?.apply {
        mSupportedPreviewSizes = parameters.supportedPreviewSizes
        requestLayout()

        try {
            setPreviewDisplay(holder)
        } catch (e: IOException) {
            e.printStackTrace()
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        startPreview()
    }
}

Java

public void setCamera(Camera camera) {
    if (mCamera == camera) { return; }

    stopPreviewAndFreeCamera();

    mCamera = camera;

    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        supportedPreviewSizes = localSizes;
        requestLayout();

        try {
            mCamera.setPreviewDisplay(holder);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        mCamera.startPreview();
    }
}

修改相機設定

相機設定會改變相機拍照的方式,包括對焦等級到曝光補償。這個範例只會變更預覽大小;詳情請參閱相機應用程式的原始碼。

Kotlin

override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) {
    mCamera?.apply {
        // Now that the size is known, set up the camera parameters and begin
        // the preview.
        parameters?.also { params ->
            params.setPreviewSize(previewSize.width, previewSize.height)
            requestLayout()
            parameters = params
        }

        // Important: Call startPreview() to start updating the preview surface.
        // Preview must be started before you can take a picture.
        startPreview()
    }
}

Java

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    // Now that the size is known, set up the camera parameters and begin
    // the preview.
    Camera.Parameters parameters = mCamera.getParameters();
    parameters.setPreviewSize(previewSize.width, previewSize.height);
    requestLayout();
    mCamera.setParameters(parameters);

    // Important: Call startPreview() to start updating the preview surface.
    // Preview must be started before you can take a picture.
    mCamera.startPreview();
}

設定預覽方向

大多數相機應用程式會將螢幕鎖定為橫向模式,因為這是相機感應器的自然方向。這項設定不會導致無法拍攝肖像模式相片,因為裝置的方向會記錄在 EXIF 標頭中。setCameraDisplayOrientation() 方法可讓您變更預覽的顯示方式,而不會影響圖片的記錄方式。不過,在 API 級別 14 之前的 Android 中,必須停止預覽,才能變更方向並重新啟動。

拍攝相片

預覽開始後,請使用 Camera.takePicture() 方法拍照。您可以建立 Camera.PictureCallbackCamera.ShutterCallback 物件,並將其傳遞至 Camera.takePicture()

如要持續擷取圖片,可以建立實作 onPreviewFrame()Camera.PreviewCallback。若要其中的內容,可以只擷取選取的預覽畫面,或設定延遲動作來呼叫 takePicture()

重新啟動預覽

使用者拍照後,必須重新啟動預覽,才能拍攝其他相片。此範例是透過按住快門按鈕來重新啟動。

Kotlin

fun onClick(v: View) {
    previewState = if (previewState == K_STATE_FROZEN) {
        camera?.startPreview()
        K_STATE_PREVIEW
    } else {
        camera?.takePicture(null, rawCallback, null)
        K_STATE_BUSY
    }
    shutterBtnConfig()
}

Java

@Override
public void onClick(View v) {
    switch(previewState) {
    case K_STATE_FROZEN:
        camera.startPreview();
        previewState = K_STATE_PREVIEW;
        break;

    default:
        camera.takePicture( null, rawCallback, null);
        previewState = K_STATE_BUSY;
    } // switch
    shutterBtnConfig();
}

停止預覽並釋出相機

應用程式完成相機拍攝後,可開始進行清除。請特別注意,必須釋出 Camera 物件,否則可能會造成其他應用程式停止運作,包括應用程式的新執行個體。

何時應停止預覽並釋出相機?如此類方法中的 Preview 類別所示,刪除預覽介面時,即應停止預覽並釋出相機。

Kotlin

override fun surfaceDestroyed(holder: SurfaceHolder) {
    // Surface will be destroyed when we return, so stop the preview.
    // Call stopPreview() to stop updating the preview surface.
    mCamera?.stopPreview()
}

/**
 * When this function returns, mCamera will be null.
 */
private fun stopPreviewAndFreeCamera() {
    mCamera?.apply {
        // Call stopPreview() to stop updating the preview surface.
        stopPreview()

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        release()

        mCamera = null
    }
}

Java

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    // Surface will be destroyed when we return, so stop the preview.
    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();
    }
}

/**
 * When this function returns, mCamera will be null.
 */
private void stopPreviewAndFreeCamera() {

    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        mCamera.release();

        mCamera = null;
    }
}

如本課程之前所述,這個程序也是 setCamera() 方法的一部分,因此初始化相機一律從停止預覽開始。