將 Camera1 遷移至 CameraX

如果您的應用程式使用自 Android 5.0 (API 級別 21) 起已淘汰的原始 Camera 類別 (「Camera1」),強烈建議您更新至最新的 Android Camera API。Android 提供 CameraX (標準化且穩固的 Jetpack 相機 API) 和 Camera2 (低階架構 API)。在大多數情況下,建議您將應用程式遷移至 CameraX。原因如下:

  • 易於使用:CameraX 可處理低階細節,這樣您就不必費心從頭打造相機體驗,而是將更多心思放在製作與眾不同的應用程式上。
  • CameraX 會為您處理片段:CameraX 可降低長期維護費用並減少使用裝置專用程式碼,能為使用者提供更優質的體驗。詳情請參閱「改善與 CameraX 的裝置相容性」網誌文章。
  • 進階功能:CameraX 經過精心設計,可將進階功能輕鬆整合至應用程式。舉例來說,您可以使用 CameraX 擴充功能,在相片上輕鬆套用散景、修容、HDR (高動態範圍) 和低亮度的夜視拍攝模式。
  • 可更新性:Android 會全年發布 CameraX 的新功能並修正錯誤。遷移至 CameraX 後,您的應用程式會透過每個 CameraX 版本取得最新的 Android 相機技術,而非只透過每年發布的 Android 版本取得這些技術。

本指南將說明相機應用程式的常見使用情境。每種情境都提供用於比較的 Camera1 和 CameraX 實作方式。

在遷移作業上,有時需要額外的彈性,才能與現有的程式碼集整合。本指南中的所有 CameraX 程式碼都提供 CameraController 實作方式,讓您能透過最簡單的方式使用 CameraX;此外,如果您需要更靈活彈性的做法,這裡也提供 CameraProvider 實作方式。為協助您判斷合適的做法,以下提供各選項的優點:

CameraController

CameraProvider

僅須少量設定程式碼 支援更多控制選項
允許 CameraX 處理更多設定程序,也就是能自動執行「輕觸對焦」和「雙指撥動縮放」等功能 應用程式開發人員可處理設定程序,因此能夠進一步掌控自訂設定,例如啟用輸出圖片旋轉功能,或是在 ImageAnalysis 中設定輸出圖片格式
必須使用 PreviewView 進行相機預覽,這樣就能讓 CameraX 提供順暢的端對端整合功能,就像我們的 ML Kit 整合功能可將機器學習模型產生的座標 (例如臉部定界框) 直接對應至預覽座標 透過自訂「Surface」進行相機預覽的功能可提供更多彈性,例如使用現有「Surface」程式碼做為應用程式其他部分的輸入內容。

如果無法順利進行遷移作業,請在 CameraX 討論群組上與我們聯絡。

遷移前置作業

比較 CameraX 和 Camera1 的使用情形

儘管程式碼的樣式可能不同,Camera1 和 CameraX 的基礎概念仍非常相似。CameraX 會將常見相機功能擷取為用途,因此許多在 Camera1 中留待開發人員處理的工作,都可由 CameraX 自動處理。CameraX 中有四種 UseCase,可用於各種相機工作:PreviewImageCaptureVideoCaptureImageAnalysis

CameraX 為開發人員處理低階細節的一個例子,就是可在多個有效 UseCase 之間共用 ViewPort。這可確保所有 UseCase 都看到相同的像素。在 Camera1 中,您必須自行管理這些細節;考量到不同裝置相機感應器和螢幕的各式長寬比,要確保預覽畫面與拍攝的相片和影片相符,可能並不容易。

再舉一個例子,CameraX 會自動在您傳遞的 Lifecycle 例項上處理 Lifecycle 回呼。也就是說,CameraX 會在整個 Android 活動生命週期中處理應用程式與相機的連線,情況包括:應用程式進入背景時關閉相機;螢幕不再需要顯示相機預覽畫面時加以移除;其他活動取得前景優先順序時 (例如視訊通話來電),暫停相機預覽畫面。

最後,CameraX 無須任何額外程式碼,即可處理旋轉和縮放。在 Activity 未鎖定螢幕方向的情況下,由於系統會在螢幕方向變更時刪除並重新建立 Activity,每次裝置旋轉時都會進行這項 UseCase 設定。結果,UseCases 每次都將目標旋轉預設為與螢幕方向一致。進一步瞭解 CameraX 的旋轉功能

在深入探討細節之前,我們先概略介紹 CameraX 的 UseCase 及其與 Camera1 應用程式之間的關聯 (CameraX 的概念會以藍色呈現,而 Camera1 的概念則是以綠色呈現)。

CameraX

CameraController/CameraProvider 設定
Preview ImageCapture 影片擷取 ImageAnalysis
管理預覽 Surface 並在 Camera 上進行設定 設定 PictureCallback 並在 Camera 上呼叫 takePicture() 依照特定順序管理 Camera 和 MediaRecorder 設定 在預覽 Surface 之上建構自訂分析程式碼
裝置專用程式碼
裝置旋轉和縮放管理
相機工作階段管理 (相機選取、生命週期管理)

Camera1

CameraX 的相容性和效能

CameraX 支援搭載 Android 5.0 (API 級別 21) 以上版本的裝置。這占現有 Android 裝置的 98% 以上。CameraX 可自動處理裝置之間的差異,減少在應用程式中使用裝置專用程式碼的需求。此外,自 Android 5.0 以來,我們已在 CameraX Test Lab 使用 150 部以上的實體裝置測試所有 Android 版本。您可以查看 Test Lab 目前裝置的完整清單。

CameraX 會使用 Executor 驅動相機堆疊。如果應用程式有特定的執行緒需求,您可以在 CameraX 上設定自己的執行程式。如果未設定,CameraX 就會建立並使用預設的最佳化內部 Executor。許多建構 CameraX 的平台 API 都需要封鎖與硬體之間的處理序間通訊 (IPC),原因是這類通訊有時可能需要數百毫秒的回應時間。因此,CameraX 只會從背景執行緒呼叫這類 API,從而確保主要執行緒不會遭到封鎖,使用者介面也能順暢運作。進一步瞭解執行緒

如果應用程式的目標市場包含低階裝置,CameraX 可利用相機限制器縮短設定時間。連接硬體元件的程序可能耗費大量時間,尤其是在低階裝置上,因此可以指定應用程式需要的相機組合。在設定期間,CameraX 只會連線至這些相機。舉例來說,如果應用程式只會使用後置鏡頭,就能使用 DEFAULT_BACK_CAMERA 進行這項設定,這樣 CameraX 就會避免初始化前置鏡頭,進而縮短延遲時間。

Android 開發概念

本指南假設您對 Android 開發作業具有一般熟悉程度。除了基本知識之外,在深入探討下方程式碼前,不妨先瞭解以下幾項實用概念:

遷移常見情境

本節說明如何將常見情境從 Camera1 遷移至 CameraX。 每種情境都會涵蓋 Camera1 實作、CameraX CameraProvider 實作以及 CameraX CameraController 實作。

選取相機

在相機應用程式中,您最想提供的其中一種功能可能是選取不同相機的方式。

Camera1

在 Camera1 中,您可以呼叫不含參數的 Camera.open() 來開啟第一個後置鏡頭,或者傳入要開啟相機的整數 ID。可能的樣式範例如下:

// Camera1: select a camera from id.

// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        camera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(TAG, "failed to open camera", e)
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    camera?.release()
    camera = null
}

CameraX:CameraController

在 CameraX 中,相機選取是由 CameraSelector 類別處理。CameraX 可讓使用者在常見情況下易於使用預設相機。您可以指定要使用預設前置鏡頭或是預設後置鏡頭。此外,CameraX 的 CameraControl 物件可讓您為應用程式輕鬆設定縮放等級,因此,如果應用程式在支援邏輯相機的裝置上執行,就會切換到適當的鏡頭。

以下 CameraX 程式碼會搭配 CameraController 使用預設後置鏡頭:

// CameraX: select a camera with CameraController

var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector

CameraX:CameraProvider

以下範例會使用 CameraProvider 選取預設前置鏡頭 (前置或後置鏡頭可與 CameraControllerCameraProvider 搭配使用):

// CameraX: select a camera with CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Set up UseCases (more on UseCases in later scenarios)
    var useCases:Array = ...

    // Set the cameraSelector to use the default front-facing (selfie)
    // camera.
    val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

如果想要控制選取的相機,也可以在 CameraX 中透過呼叫 getAvailableCameraInfos() 使用 CameraProvider,這樣就能取得 CameraInfo 物件,用來檢查 isFocusMeteringSupported() 等特定相機屬性。接著,您可以將其轉換為 CameraSelector,如上述範例般搭配 CameraInfo.getCameraSelector() 方法使用。

您可以使用 Camera2CameraInfo 類別取得各相機的詳細資訊。使用所需相機資料的鍵呼叫 getCameraCharacteristic()。您可以查看 CameraCharacteristics 類別,取得可用於查詢的鍵完整清單。

以下提供可自行定義的自訂 checkFocalLength() 函式範例:

// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().

val cameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

顯示預覽畫面

大多數的相機應用程式都需要於某個時間點,在螢幕上顯示攝影機畫面。使用 Camera1 時,必須正確管理生命週期回呼,也需要判斷預覽畫面的旋轉和縮放情形。

此外,在 Camera1 中,您必須決定要使用 TextureView 還是 SurfaceView 做為預覽畫面。這兩種選項各有優缺點,但無論在哪種情況下,Camera1 都會要求您妥善處理旋轉和縮放情形。另一方面,CameraX 的 PreviewView 同時提供 TextureViewSurfaceView 的基礎實作。CameraX 會根據裝置類型和執行應用程式的 Android 版本等因素,判斷哪一種是最佳實作方式。如果任一種實作都相容,您可以使用 PreviewView.ImplementationMode 宣告偏好的實作方式。COMPATIBLE 選項會使用 TextureView 進行預覽,而 PERFORMANCE 值則會使用 SurfaceView (如果可行)。

Camera1

如要顯示預覽畫面,您必須自行編寫 Preview 類別並實作 android.view.SurfaceHolder.Callback 介面,用於將圖像資料從相機硬體傳送至應用程式。接著,您必須先將 Preview 類別傳遞至 Camera 物件,才能啟動即時圖像預覽。

// Camera1: set up a camera preview.

class Preview(
        context: Context,
        private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {

    private val holder: SurfaceHolder = holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // The Surface has been created, now tell the camera
        // where to draw the preview.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "error setting camera preview", e)
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Take care of releasing the Camera preview in your activity.
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                w: Int, h: Int) {
        // If your preview can change or rotate, take care of those
        // events here. Make sure to stop the preview before resizing
        // or reformatting it.
        if (holder.surface == null) {
            return  // The preview surface does not exist.
        }

        // Stop preview before making changes.
        try {
            camera.stopPreview()
        } catch (e: Exception) {
            // Tried to stop a non-existent preview; nothing to do.
        }

        // Set preview size and make any resize, rotate or
        // reformatting changes here.

        // Start preview with new settings.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: Exception) {
                Log.d(TAG, "error starting camera preview", e)
            }
        }
    }
}

class CameraActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private var camera: Camera? = null
    private var preview: Preview? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create an instance of Camera.
        camera = getCameraInstance()

        preview = camera?.let {
            // Create the Preview view.
            Preview(this, it)
        }

        // Set the Preview view as the content of the activity.
        val cameraPreview: FrameLayout = viewBinding.cameraPreview
        cameraPreview.addView(preview)
    }
}

CameraX:CameraController

在 CameraX 中,開發人員需要管理的項目會大幅減少。如果您使用了 CameraController,就必須同時使用 PreviewView。這表示 Preview UseCase 會以隱含方式使得設定工作大幅減少:

// CameraX: set up a camera preview with a CameraController.

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create the CameraController and set it on the previewView.
        var cameraController = LifecycleCameraController(baseContext)
        cameraController.bindToLifecycle(this)
        val previewView: PreviewView = viewBinding.cameraPreview
        previewView.controller = cameraController
    }
}

CameraX:CameraProvider

使用 CameraX 的 CameraProvider 時,您不必使用 PreviewView,但仍可大幅簡化 Camera1 的預覽設定。為了進行示範,這個範例使用了 PreviewView,但如有更複雜的需求,您可以編寫自訂的 SurfaceProvider 來傳入 setSurfaceProvider()

在本例中,Preview UseCase 並不像搭配 CameraController 時以隱含方式執行,因此您必須自行設定:

// CameraX: set up a camera preview with a CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Create Preview UseCase.
    val preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(
                viewBinding.viewFinder.surfaceProvider
            )
        }

    // Select default back camera.
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

輕觸對焦

相機預覽畫面顯示在螢幕上時,常見的控制方式是在使用者輕觸預覽畫面時設定焦點。

Camera1

如要在 Camera1 中實作輕觸對焦功能,您必須計算最佳焦點 Area,用來指示 Camera 應嘗試聚焦的位置。這個 Area 會傳入 setFocusAreas()。此外,您必須在 Camera 上設定相容的聚焦模式。只有目前處於 FOCUS_MODE_AUTOFOCUS_MODE_MACROFOCUS_MODE_CONTINUOUS_VIDEOFOCUS_MODE_CONTINUOUS_PICTURE 聚焦模式時,焦點區域才會生效。

每個 Area 都是具有指定權重的矩形。權重是介於 1 到 1000 之間的值,如果已設定多個值,系統會根據這個值決定焦點 Areas 的優先順序。這個範例僅使用一個 Area,因此權重值沒有影響。矩形的座標範圍可介於 -1000 到 1000 之間,左上角的點為 (-1000, -1000),右下角的點為 (1000, 1000)。其方向是相對於感應器的方向,也就是感應器偵測到的方向。方向不受 Camera.setDisplayOrientation() 的旋轉或鏡像影響,因此您必須將觸控事件座標轉換為感應器座標。

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX:CameraController

CameraController 會監聽 PreviewView 的觸控事件,自動處理輕觸對焦作業。您可以使用 setTapToFocusEnabled() 啟用及停用輕觸對焦功能,並透過對應的 getter isTapToFocusEnabled() 檢查這個值。

getTapToFocusState() 方法會傳回 LiveData 物件,用於追蹤 CameraController 的焦點狀態變更情形。

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer { state ->
    when (state) {
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

CameraX:CameraProvider

使用 CameraProvider 時,需要進行一些設定,輕觸對焦功能才能正常運作。這個範例假設您使用的是 PreviewView。如果不是,就必須調整邏輯來套用至自訂 Surface

使用 PreviewView 的步驟如下:

  1. 設定手勢偵測工具來處理輕觸事件。
  2. 使用輕觸事件,利用 MeteringPointFactory.createPoint() 建立 MeteringPoint
  3. 使用 MeteringPoint 建立 FocusMeteringAction
  4. Camera 上,使用從 bindToLifecycle() 傳回的 CameraControl 物件呼叫 startFocusAndMetering(),並傳入 FocusMeteringAction
  5. (選用) 對 FocusMeteringResult 做出回應。
  6. 設定手勢偵測工具,回應 PreviewView.setOnTouchListener() 中的觸控事件。
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    // See pinch-to-zooom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

雙指撥動縮放

縮放預覽畫面是另一種直接操控相機預覽的常見方式。隨著裝置的鏡頭數量逐漸增加,使用者也預期系統會自動選取具有最佳焦距的鏡頭,產生縮放效果。

Camera1

使用 Camera1 的縮放方式有兩種。Camera.startSmoothZoom() 方法會採用動畫方式,從目前縮放等級轉換成傳入的縮放等級。Camera.Parameters.setZoom() 方法則會直接跳到傳入的縮放等級。在使用任何一種方法前,請先分別呼叫 isSmoothZoomSupported()isZoomSupported(),確保 Camera 能提供所需的縮放方法。

這個範例使用 setZoom() 實作雙指撥動縮放,因為預覽介面的觸控事件監聽器會在雙指撥動手勢發生時持續觸發事件,每次都會立即更新縮放等級。ZoomTouchListener 類別定義如下,應設為預覽畫面觸控事件監聽器的回呼。

// Camera1: implement pinch-to-zoom.

// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val parameters = try {
                camera.parameters
            } catch (e: RuntimeException) {
                return false
            }

            // In case there is any focus happening, stop it.
            camera.cancelAutoFocus()

            // Set the zoom level on the Camera.Parameters, and set
            // the Parameters back onto the Camera.
            val currentZoom = parameters.zoom
            parameters.setZoom(detector.scaleFactor * currentZoom)
        camera.setParameters(parameters)
            return true
        }
    }
)

// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean =
        scaleGestureDetector.onTouchEvent(event)
}

// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
    view.setOnTouchListener(ZoomTouchListener())
}

CameraX:CameraController

與輕觸對焦功能相似,CameraController 會監聽 PreviewView 的觸控事件,自動處理雙指撥動縮放作業。您可以使用 setPinchToZoomEnabled() 啟用及停用雙指撥動縮放功能,並透過對應的 getter isPinchToZoomEnabled() 檢查這個值。

getZoomState() 方法會傳回 LiveData 物件,用於追蹤 CameraControllerZoomState 變更情形。

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX:CameraProvider

如要使用 CameraProvider 進行雙指撥動縮放,需要進行一些設定。如果您不使用 PreviewView,就必須調整邏輯來套用至自訂 Surface

使用 PreviewView 的步驟如下:

  1. 設定縮放手勢偵測工具來處理雙指撥動事件。
  2. Camera.CameraInfo 物件取得 ZoomState,系統會在您呼叫 bindToLifecycle() 時傳回 Camera 例項。
  3. 如果 ZoomState 有提供 zoomRatio 的值,請將其儲存為目前的縮放比例。如果 ZoomState 上沒有 zoomRatio,則使用相機的預設縮放比率 (1.0)。
  4. 使用 scaleFactor 取得目前的縮放比例結果,用來判斷新的縮放比例,並將其傳入 CameraControl.setZoomRatio()
  5. 設定手勢偵測工具,回應 PreviewView.setOnTouchListener() 中的觸控事件。
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        // See pinch-to-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

拍攝相片

本節說明如何在按下快門按鈕、計時器時間已到,或執行任何其他所選事件的情況下觸發相片拍攝。

Camera1

在 Camera1 中,您必須先定義 Camera.PictureCallback,用來在系統要求時管理圖片資料。以下是用於處理 JPEG 圖片資料的 PictureCallback 簡易範例:

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

接下來,您應在每次想要拍照時,對 Camera 例項呼叫 takePicture() 方法。這個 takePicture() 方法針對不同資料類型提供三個不同參數。第一個參數適用於 ShutterCallback (本範例中並未定義)。第二個參數是用來讓 PictureCallback 處理原始 (未壓縮) 的相機資料。第三個參數就是本範例使用的參數,因為這是處理 JPEG 圖片資料的 PictureCallback

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX:CameraController

CameraX 的 CameraController 會自行實作 takePicture() 方法,簡化 Camera1 的圖片拍攝方式。以下範例會定義用來設定 MediaStore 項目的函式,並拍攝要儲存的相片。

// CameraX: define a function that uses CameraController to take a photo.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun takePhoto() {
   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata.
   val outputOptions = ImageCapture.OutputFileOptions
       .Builder(context.getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
       .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken.
   cameraController.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(e: ImageCaptureException) {
               Log.e(TAG, "photo capture failed", e)
           }

           override fun onImageSaved(
               output: ImageCapture.OutputFileResults
           ) {
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}

CameraX:CameraProvider

使用 CameraProvider 拍照的運作方式幾乎與 CameraController 相同,但您必須先建立並繫結 ImageCapture UseCase,才能提供物件供 takePicture() 呼叫:

// CameraX: create and bind an ImageCapture UseCase.

// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null

...

// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()

...

// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

接下來,您可以在每次想要拍照時呼叫 ImageCapture.takePicture()。如需 takePhoto() 函式的完整範例,請參閱本節中的 CameraController 程式碼。

// CameraX: define a function that uses CameraController to take a photo.

private fun takePhoto() {
    // Get a stable reference of the modifiable ImageCapture UseCase.
    val imageCapture = imageCapture ?: return

    ...

    // Call takePicture on imageCapture instance.
    imageCapture.takePicture(
        ...
    )
}

錄製影片

相較於我們目前為止介紹的情境,錄製影片會更為複雜。這套程序的每個部分都必須正確設定,而且通常會以特定順序進行。此外,您可能需要驗證影片和音訊是否同步,或是處理其他因裝置引起的不一致問題。

您將瞭解到,CameraX 會再次為您處理眾多複雜工作。

Camera1

使用 Camera1 錄製影片需要謹慎管理 CameraMediaRecorder,而且必須以特定順序呼叫這些方法。您必須按照以下順序操作,應用程式才能正常運作:

  1. 開啟相機。
  2. 準備並啟動預覽畫面 (應用程式通常會在錄影前顯示視訊)。
  3. 呼叫 Camera.unlock() 將相機解鎖,供 MediaRecorder 使用。
  4. MediaRecorder 呼叫下列方法,設定錄製功能:
    1. Camera 例項連結至 setCamera(camera)
    2. 呼叫 setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    3. 呼叫 setVideoSource(MediaRecorder.VideoSource.CAMERA)
    4. 呼叫 setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) 可設定品質。如需所有品質選項,請參閱 CamcorderProfile
    5. 呼叫 setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
    6. 如果應用程式有提供影片預覽,請呼叫 setPreviewDisplay(preview?.holder?.surface)
    7. 呼叫 setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    8. 呼叫 setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
    9. 呼叫 setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
    10. 呼叫 prepare() 完成 MediaRecorder 的設定。
  5. 如要開始錄影,請呼叫 MediaRecorder.start()
  6. 如要停止錄影,請呼叫下列方法。同樣地,請確實按照以下順序進行:
    1. 呼叫 MediaRecorder.stop()
    2. 如有需要,呼叫 MediaRecorder.reset() 來移除目前的 MediaRecorder 設定。
    3. 呼叫 MediaRecorder.release()
    4. 藉由呼叫 Camera.lock() 鎖定相機,供未來的 MediaRecorder 工作階段使用。
  7. 如要停止預覽,請呼叫 Camera.stopPreview()
  8. 最後,如要釋出 Camera 供其他程序使用,請呼叫 Camera.release()

以下內容結合了上述所有步驟的說明:

// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.

// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false

...

private fun prepareMediaRecorder(): Boolean {
    mediaRecorder = MediaRecorder()

    // Unlock and set camera to MediaRecorder.
    camera?.unlock()

    mediaRecorder?.run {
        setCamera(camera)

        // Set the audio and video sources.
        setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        setVideoSource(MediaRecorder.VideoSource.CAMERA)

        // Set a CamcorderProfile (requires API Level 8 or higher).
        setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))

        // Set the output file.
        setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())

        // Set the preview output.
        setPreviewDisplay(preview?.holder?.surface)

        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
        setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)

        // Prepare configured MediaRecorder.
        return try {
            prepare()
            true
        } catch (e: IllegalStateException) {
            Log.d(TAG, "preparing MediaRecorder failed", e)
            releaseMediaRecorder()
            false
        } catch (e: IOException) {
            Log.d(TAG, "setting MediaRecorder file failed", e)
            releaseMediaRecorder()
            false
        }
    }
    return false
}

private fun releaseMediaRecorder() {
    mediaRecorder?.reset()
    mediaRecorder?.release()
    mediaRecorder = null
    camera?.lock()
}

private fun startStopVideo() {
    if (isRecording) {
        // Stop recording and release camera.
        mediaRecorder?.stop()
        releaseMediaRecorder()
        camera?.lock()
        isRecording = false

        // This is a good place to inform user that video recording has stopped.
    } else {
        // Initialize video camera.
        if (prepareVideoRecorder()) {
            // Camera is available and unlocked, MediaRecorder is prepared, now
            // you can start recording.
            mediaRecorder?.start()
            isRecording = true

            // This is a good place to inform the user that recording has
            // started.
        } else {
            // Prepare didn't work, release the camera.
            releaseMediaRecorder()

            // Inform user here.
        }
    }
}

CameraX:CameraController

您可以使用 CameraX 的 CameraController 獨立切換 ImageCaptureVideoCaptureImageAnalysis UseCase,但前提是可同時使用 UseCase 清單。系統預設會啟用 ImageCaptureImageAnalysis UseCase,因此您不必呼叫 setEnabledUseCases() 即可拍照。

如要使用 CameraController 錄製影片,就必須先呼叫 setEnabledUseCases() 才能執行 VideoCapture UseCase

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

想要開始錄製影片時,您可以呼叫 CameraController.startRecording() 函式。這個函式可將錄製的影片儲存至 File,如下方範例所示。此外,您必須傳遞 Executor 和實作 OnVideoSavedCallback 的類別,用來處理成功和錯誤回呼。錄影結束時,請呼叫 CameraController.stopRecording()

注意:如果您使用的是 CameraX 1.3.0-alpha02 以上版本,還可以透過額外的 AudioConfig 參數啟用或停用影片的音訊錄音功能 如要啟用音訊錄音功能,您必須具備麥克風權限。此外,1.3.0-alpha02 已移除 stopRecording() 方法,而 startRecording() 會傳回 Recording 物件,可用於暫停、繼續及停止錄影。

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX:CameraProvider

如果您要使用 CameraProvider,就必須建立 VideoCapture UseCase 並傳入 Recorder 物件。您可以透過 Recorder.Builder 設定影片畫質,也可以視需要設定 FallbackStrategy,用來處理裝置無法符合所需品質規格的情況。接著,將 VideoCapture 例項繫結至附有其他 UseCaseCameraProvider

// CameraX: create and bind a VideoCapture UseCase with CameraProvider.

// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null

...

// Create a Recorder instance to set on a VideoCapture instance (can be
// added with other UseCase definitions).
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
videoCapture = VideoCapture.withOutput(recorder)

...

// Bind UseCases to camera (adding videoCapture along with preview here, but
// preview is not required to use videoCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, videoCapture)

在這個階段,您可以透過 videoCapture.output 屬性存取 RecorderRecorder 可啟動儲存至 FileParcelFileDescriptorMediaStore 的影片錄製功能。本範例使用的是 MediaStore

您可以對 Recorder 呼叫幾個方法來進行準備作業。呼叫 prepareRecording() 可以設定 MediaStore 輸出選項。如果應用程式具備使用裝置麥克風的權限,請同時呼叫 withAudioEnabled()。接著,呼叫 start() 開始錄製,並傳入背景資訊和 Consumer<VideoRecordEvent> 事件監聽器來處理影片錄製事件。如果成功,傳回的 Recording 可用於暫停、繼續或停止錄製。

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.stop()
       recording = null
       return
   }

   // Create and start a new recording session.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
       .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()

   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .withAudioEnabled()
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(
                           baseContext, msg, Toast.LENGTH_SHORT
                       ).show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "video capture ends with error",
                             recordEvent.error)
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}

其他資源

我們在 Camera Samples GitHub 存放區中提供了多個完整的 CameraX 應用程式。這些範例會說明如何將本指南所述的情境套用到發展成熟的 Android 應用程式當中。

如果您在遷移至 CameraX 時需要額外支援,或對 Android Camera API 套件有任何疑問,請透過 CameraX 討論群組與我們聯絡。