螢幕閃光燈

螢幕閃光燈又稱為前置閃光燈或自拍閃光燈,在低光源環境下使用前置鏡頭拍照時,會利用手機螢幕亮度照亮主體。許多原生相機應用程式和社群媒體應用程式都支援這項功能。大多數人在自拍時,都會將手機拿得夠近,因此這種做法很有效。

不過,開發人員很難正確實作這項功能,並在不同裝置上維持良好的擷取品質。本指南說明如何使用 Camera2 (低階 Android 相機架構 API) 正確導入這項功能。

一般工作流程

如要正確實作這項功能,有兩個關鍵因素:使用預先擷取測光序列 (自動曝光預先擷取),以及作業時機。一般工作流程如圖 1 所示。

流程圖:說明如何在 Camera2 中使用螢幕閃光燈 UI。
圖 1. 實作螢幕閃光燈的一般工作流程。

如要使用螢幕閃光燈功能拍攝圖片,請按照下列步驟操作。

  1. 套用螢幕閃光燈所需的 UI 變更,以便使用裝置螢幕拍照時提供充足的光線。對於一般用途,Google 建議採用下列 UI 變更 (我們在測試中也使用這些變更):
    • 應用程式畫面覆蓋白色疊加層。
    • 螢幕亮度已調到最高。
  2. 如果支援,請將自動曝光 (AE) 模式設為 CONTROL_AE_MODE_ON_EXTERNAL_FLASH。
  3. 使用 CONTROL_AE_PRECAPTURE_TRIGGER 觸發預先擷取測光序列。
  4. 等待自動曝光 (AE) 和自動白平衡 (AWB) 收斂。

  5. 收斂後,系統會使用應用程式的一般相片拍攝流程。

  6. 將擷取要求傳送至架構。

  7. 等待接收擷取結果。

  8. 如果已設定 CONTROL_AE_MODE_ON_EXTERNAL_FLASH,請重設 AE 模式。

  9. 清除螢幕閃光燈的 UI 變更。

Camera2 程式碼範例

在應用程式畫面上覆蓋白色

在應用程式的版面配置 XML 檔案中新增 View。在螢幕閃光燈拍攝期間,檢視畫面有足夠的高度,可置於所有其他 UI 元素之上。根據預設,這個檢視畫面會保持隱藏,只有在套用螢幕閃光燈 UI 變更時才會顯示。

在以下程式碼範例中,檢視畫面使用白色 (#FFFFFF)。應用程式可以根據需求選擇顏色,或為使用者提供多種顏色。

<View
    android:id="@+id/white_color_overlay"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:visibility="invisible"
    android:elevation="8dp" />

將螢幕亮度調到最高

在 Android 應用程式中,有多種方式可以變更螢幕亮度。其中一種直接的方法,是變更活動視窗參考資料中的 screenBrightness WindowManager 參數。

private var previousBrightness: Float = -1.0f

private fun maximizeScreenBrightness() {
    activity?.window?.let { window ->
        window.attributes?.apply {
            previousBrightness = screenBrightness
            screenBrightness = 1f
            window.attributes = this
        }
    }
}

private fun restoreScreenBrightness() {
    activity?.window?.let { window ->
        window.attributes?.apply {
            screenBrightness = previousBrightness
            window.attributes = this
        }
    }
}

將 AE 模式設為 CONTROL_AE_MODE_ON_EXTERNAL_FLASH

CONTROL_AE_MODE_ON_EXTERNAL_FLASH 適用於 API 級別 28 以上版本。不過,並非所有裝置都支援這個 AE 模式,因此請檢查 AE 模式是否可用,並據此設定值。如要檢查可用性,請使用 CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES。

private val characteristics: CameraCharacteristics by lazy {
    cameraManager.getCameraCharacteristics(cameraId)
}

@RequiresApi(Build.VERSION_CODES.P)
private fun isExternalFlashAeModeAvailable() =
    characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)
        ?.contains(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) ?: false

如果應用程式已設定重複擷取要求 (預覽時必須設定),則 AE 模式必須設為重複要求。否則,在下一次重複擷取時,可能會被預設或其他使用者設定的 AE 模式覆寫。如果發生這種情況,相機可能沒有足夠的時間,執行外接閃光燈 AE 模式通常會執行的所有作業。

為確保相機完整處理 AE 模式更新要求,請在重複擷取回呼中檢查擷取結果,並等待結果中的 AE 模式更新。

擷取可等待 AE 模式更新的回呼

下列程式碼片段說明如何完成這項操作。

private val repeatingCaptureCallback = object : CameraCaptureSession.CaptureCallback() {
    private var targetAeMode: Int? = null
    private var aeModeUpdateDeferred: CompletableDeferred<Unit>? = null

    suspend fun awaitAeModeUpdate(targetAeMode: Int) {
        this.targetAeMode = targetAeMode
        aeModeUpdateDeferred = CompletableDeferred()
        // Makes the current coroutine wait until aeModeUpdateDeferred is completed. It is
        // completed once targetAeMode is found in the following capture callbacks.
        aeModeUpdateDeferred?.await()
    }

    private fun process(result: CaptureResult) {
        // Checks if AE mode is updated and completes any awaiting Deferred.
        aeModeUpdateDeferred?.let {
            val aeMode = result[CaptureResult.CONTROL_AE_MODE]
            if (aeMode == targetAeMode) {
                it.complete(Unit)
            }
        }
    }

    override fun onCaptureCompleted(
        session: CameraCaptureSession,
        request: CaptureRequest,
        result: TotalCaptureResult
    ) {
        super.onCaptureCompleted(session, request, result)
        process(result)
    }
}

設定重複要求,啟用或停用 AE 模式

設定擷取回呼後,下列程式碼範例說明如何設定重複要求。

/** [HandlerThread] where all camera operations run */
private val cameraThread = HandlerThread("CameraThread").apply { start() }

/** [Handler] corresponding to [cameraThread] */
private val cameraHandler = Handler(cameraThread.looper)

private suspend fun enableExternalFlashAeMode() {
    if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) {
        session.setRepeatingRequest(
            camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
                addTarget(previewSurface)
                set(
                    CaptureRequest.CONTROL_AE_MODE,
                    CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH
                )
            }.build(),
            repeatingCaptureCallback, cameraHandler
        )

        // Wait for the request to be processed by camera.
        repeatingCaptureCallback.awaitAeModeUpdate(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH)
    }
}

private fun disableExternalFlashAeMode() {
    if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) {
        session.setRepeatingRequest(
            camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
                addTarget(previewSurface)
            }.build(),
            repeatingCaptureCallback, cameraHandler
        )
    }
}

觸發預先拍攝序列

如要觸發預先擷取測光序列,您可以提交 CaptureRequest,並將 CONTROL_AE_PRECAPTURE_TRIGGER_START 值設為要求。您必須等待要求處理完畢,然後等待 AE 和 AWB 收斂。

雖然預先擷取會透過單一擷取要求觸發,但等待 AE 和 AWB 收斂確實需要更複雜的程序。您可以使用設為重複要求的擷取回呼,追蹤 AE 狀態和 AWB 狀態。

更新同一個重複回呼,可簡化程式碼。應用程式通常需要預覽畫面,因此會在設定相機時設定重複要求。因此,您可以將重複擷取回呼設為該初始重複要求一次,然後重複使用該回呼來檢查結果和等待。

擷取回呼程式碼更新,等待收斂

如要更新重複擷取回呼,請使用下列程式碼片段。

private val repeatingCaptureCallback = object : CameraCaptureSession.CaptureCallback() {
    private var targetAeMode: Int? = null
    private var aeModeUpdateDeferred: CompletableDeferred<Unit>? = null

    private var convergenceDeferred: CompletableDeferred<Unit>? = null

    suspend fun awaitAeModeUpdate(targetAeMode: Int) {
        this.targetAeMode = targetAeMode
        aeModeUpdateDeferred = CompletableDeferred()
        // Makes the current coroutine wait until aeModeUpdateDeferred is completed. It is
        // completed once targetAeMode is found in the following capture callbacks.
        aeModeUpdateDeferred?.await()
    }

    suspend fun awaitAeAwbConvergence() {
        convergenceDeferred = CompletableDeferred()
        // Makes the current coroutine wait until convergenceDeferred is completed, it will be
        // completed once both AE & AWB are reported as converged in the capture callbacks below.
        convergenceDeferred?.await()
    }

    private fun process(result: CaptureResult) {
        // Checks if AE mode is updated and completes any awaiting Deferred.
        aeModeUpdateDeferred?.let {
            val aeMode = result[CaptureResult.CONTROL_AE_MODE]
            if (aeMode == targetAeMode) {
                it.complete(Unit)
            }
        }

        // Checks for convergence and completes any awaiting Deferred.
        convergenceDeferred?.let {
            val aeState = result[CaptureResult.CONTROL_AE_STATE]
            val awbState = result[CaptureResult.CONTROL_AWB_STATE]

            val isAeReady = (
                aeState == null ||
                    // May be null in some devices (e.g. legacy camera HW level).
                    aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED ||
                    aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED
                )

            val isAwbReady = (
                awbState == null ||
                    // May be null in some devices (e.g. legacy camera HW level).
                    awbState == CaptureResult.CONTROL_AWB_STATE_CONVERGED
                )

            if (isAeReady && isAwbReady) {
                // if any non-null convergenceDeferred is set, complete it.
                it.complete(Unit)
            }
        }
    }

    override fun onCaptureCompleted(
        session: CameraCaptureSession,
        request: CaptureRequest,
        result: TotalCaptureResult
    ) {
        super.onCaptureCompleted(session, request, result)
        process(result)
    }
}

在相機設定期間,將回呼設為重複要求

您可以使用下列程式碼範例,在初始化期間將回呼設為重複要求。

// Open the selected camera.
camera = openCamera(cameraManager, cameraId, cameraHandler)

// Creates list of Surfaces where the camera will output frames.
val targets = listOf(previewSurface, imageReaderSurface)

// Start a capture session using our open camera and list of Surfaces where frames will go.
session = createCameraCaptureSession(camera, targets, cameraHandler)

val captureRequest = camera.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW
).apply { addTarget(previewSurface) }

// This will keep sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called.
session.setRepeatingRequest(captureRequest.build(), repeatingCaptureCallback, cameraHandler)

預先擷取序列觸發和等待

設定回呼後,您可以使用下列程式碼範例,觸發並等待預先擷取序列。

private suspend fun runPrecaptureSequence() {
    // Creates a new capture request with CONTROL_AE_PRECAPTURE_TRIGGER_START.
    val captureRequest = session.device.createCaptureRequest(
        CameraDevice.TEMPLATE_PREVIEW
    ).apply {
        addTarget(previewSurface)
        set(
            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
        )
    }

    val precaptureDeferred = CompletableDeferred<Unit>()
    session.capture(
        captureRequest.build(),
        object : CameraCaptureSession.CaptureCallback() {
            override fun onCaptureCompleted(
                session: CameraCaptureSession,
                request: CaptureRequest,
                result: TotalCaptureResult
            ) {
                // Waiting for this callback ensures the precapture request has been processed.
                precaptureDeferred.complete(Unit)
            }
        },
        cameraHandler
    )

    precaptureDeferred.await()

    // Precapture trigger request has been processed, we can wait for AE & AWB convergence now.
    repeatingCaptureCallback.awaitAeAwbConvergence()
}

整合所有內容

所有主要元件都準備就緒後,只要需要拍照 (例如使用者點按拍照按鈕),所有步驟就會按照上述討論和程式碼範例中的順序執行。

// User clicks captureButton to take picture.
captureButton.setOnClickListener {
    // Apply the screen flash related UI changes.
    whiteColorOverlayView.visibility = View.VISIBLE
    maximizeScreenBrightness()

    // Launch on the main thread, then switch to IO for background operations.
    lifecycleScope.launch {
        // Perform I/O heavy operations in a different scope.
        withContext(Dispatchers.IO) {
            // Enable external flash AE mode and wait for it to be processed.
            enableExternalFlashAeMode()

            // Run precapture sequence and wait for it to complete.
            runPrecaptureSequence()

            // Start taking picture and wait for it to complete.
            takePhoto()

            disableExternalFlashAeMode()
        }

        // Back on the main thread automatically.
        // Clear the screen flash related UI changes.
        restoreScreenBrightness()
        whiteColorOverlayView.visibility = View.INVISIBLE
    }
}

範例圖片

以下範例說明螢幕閃光燈實作不正確和正確時的情況。

不當做法

如果螢幕閃光燈未正確實作,在多個拍攝、裝置和光源條件下,結果會不一致。拍攝的圖片通常會有曝光不佳或色調問題。在某些裝置上,這類錯誤在特定光線條件下會更明顯,例如低光源環境,而非完全黑暗的環境。

下表列出這類問題的示例。這些相片是在 CameraX 實驗室基礎架構中拍攝,光源維持暖白色。這個暖白光源可讓您瞭解藍色色調是實際問題,而非光源的副作用。

環境 曝光不足 過度曝光 色調
黑暗環境 (除了手機外沒有其他光源) 幾乎全黑的相片 過度調亮的相片 帶有紫色色調的相片
低光源 (額外 ~3 lux 光源) 相片有點暗 過度調亮的相片 偏藍的相片

只要方法正確

如果對相同裝置和條件使用標準實作,結果如下表所示。

環境 曝光不足 (已修正) 過度曝光 (已修正) 色調 (固定)
黑暗環境 (除了手機外沒有其他光源) 清除相片 清除相片 相片清晰,沒有任何色調
低光源 (額外 ~3 lux 光源) 清除相片 清除相片 清晰且無色調的相片

如您所見,標準導入方式可大幅提升圖片品質。