螢幕閃光燈

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

不過,開發人員很難正確實作這項功能,並在各裝置上維持一致的良好擷取品質。本指南將說明如何使用 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 參數。

Kotlin

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
        }
    }
}

Java

private float mPreviousBrightness = -1.0f;

private void maximizeScreenBrightness() {
    if (getActivity() == null || getActivity().getWindow() == null) {
        return;
    }

    Window window = getActivity().getWindow();
    WindowManager.LayoutParams attributes = window.getAttributes();

    mPreviousBrightness = attributes.screenBrightness;
    attributes.screenBrightness = 1f;
    window.setAttributes(attributes);
}

private void restoreScreenBrightness() {
    if (getActivity() == null || getActivity().getWindow() == null) {
        return;
    }

    Window window = getActivity().getWindow();
    WindowManager.LayoutParams attributes = window.getAttributes();

    attributes.screenBrightness = mPreviousBrightness;
    window.setAttributes(attributes);
}

將 AE 模式設為 CONTROL_AE_MODE_ON_EXTERNAL_FLASH

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

Kotlin

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

Java

try {
    mCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
} catch (CameraAccessException e) {
    e.printStackTrace();
}

@RequiresApi(Build.VERSION_CODES.P)
private boolean isExternalFlashAeModeAvailable() {
    int[] availableAeModes = mCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);

    for (int aeMode : availableAeModes) {
        if (aeMode == CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) {
            return true;
        }
    }
    return false;
}

如果應用程式有重複擷取要求集,則 AE 模式必須設為重複要求。否則,在下一次重複擷取時,系統可能會使用預設值或其他使用者設定的 AE 模式覆寫該值。在這種情況下,相機可能沒有足夠的時間執行外部閃光燈 AE 模式的所有正常操作。

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

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

以下程式碼片段說明如何達成這項目標。

Kotlin

private val repeatingCaptureCallback = object : CameraCaptureSession.CaptureCallback() {
    private var targetAeMode: Int? = null
    private var aeModeUpdateDeferred: CompletableDeferred? = 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)
    }
}

Java

static class AwaitingCaptureCallback extends CameraCaptureSession.CaptureCallback {
    private int mTargetAeMode;
    private CountDownLatch mAeModeUpdateLatch = null;

    public void awaitAeModeUpdate(int targetAeMode) {
        mTargetAeMode = targetAeMode;
        mAeModeUpdateLatch = new CountDownLatch(1);
        // Makes the current thread wait until mAeModeUpdateLatch is released, it will be
        // released once targetAeMode is found in the capture callbacks below
        try {
            mAeModeUpdateLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void process(CaptureResult result) {
        // Checks if AE mode is updated and decrements the count of any awaiting latch
        if (mAeModeUpdateLatch != null) {
            int aeMode = result.get(CaptureResult.CONTROL_AE_MODE);
            if (aeMode == mTargetAeMode) {
                mAeModeUpdateLatch.countDown();
            }
        }
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
            @NonNull CaptureRequest request,
            @NonNull TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);
        process(result);
    }
}

private final AwaitingCaptureCallback mRepeatingCaptureCallback = new AwaitingCaptureCallback();

設定重複請求,以便啟用或停用 AE 模式

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

Kotlin

/** [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
        )
    }
}

Java

private void setupCameraThread() {
    // HandlerThread where all camera operations run
    HandlerThread cameraThread = new HandlerThread("CameraThread");
    cameraThread.start();

    // Handler corresponding to cameraThread
    mCameraHandler = new Handler(cameraThread.getLooper());
}

private void enableExternalFlashAeMode() {
    if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) {
        try {
            CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            requestBuilder.addTarget(mPreviewSurface);
            requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH);
            mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

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

private void disableExternalFlashAeMode() {
    if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) {
        try {
            CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            requestBuilder.addTarget(mPreviewSurface);
            mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
}

觸發預先拍攝序列

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

雖然預擷取事件會使用單一擷取要求觸發,但等待自動曝光和自動白平功能收斂的過程確實需要更複雜的處理。您可以使用設定為重複要求的擷取回呼,追蹤自動曝光狀態自動白平狀態

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

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

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

Kotlin

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

    private var convergenceDeferred: CompletableDeferred? = 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)
    }
}

Java

static class AwaitingCaptureCallback extends CameraCaptureSession.CaptureCallback {
    private int mTargetAeMode;
    private CountDownLatch mAeModeUpdateLatch = null;

    private CountDownLatch mConvergenceLatch = null;

    public void awaitAeModeUpdate(int targetAeMode) {
        mTargetAeMode = targetAeMode;
        mAeModeUpdateLatch = new CountDownLatch(1);
        // Makes the current thread wait until mAeModeUpdateLatch is released, it will be
        // released once targetAeMode is found in the capture callbacks below
        try {
            mAeModeUpdateLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void awaitAeAwbConvergence() {
        mConvergenceLatch = new CountDownLatch(1);
        // Makes the current coroutine wait until mConvergenceLatch is released, it will be
        // released once both AE & AWB are reported as converged in the capture callbacks below
        try {
            mConvergenceLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void process(CaptureResult result) {
        // Checks if AE mode is updated and decrements the count of any awaiting latch
        if (mAeModeUpdateLatch != null) {
            int aeMode = result.get(CaptureResult.CONTROL_AE_MODE);
            if (aeMode == mTargetAeMode) {
                mAeModeUpdateLatch.countDown();
            }
        }

        // Checks for convergence and decrements the count of any awaiting latch
        if (mConvergenceLatch != null) {
            Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
            Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE);

            boolean 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
            );

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

            if (isAeReady && isAwbReady) {
                mConvergenceLatch.countDown();
                mConvergenceLatch = null;
            }
        }
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
            @NonNull CaptureRequest request,
            @NonNull TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);
        process(result);
    }
}

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

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

Kotlin

// 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)

Java

// Open the selected camera
mCamera = openCamera(mCameraManager, mCameraId, mCameraHandler);

// Creates list of Surfaces where the camera will output frames
List targets = new ArrayList<>(Arrays.asList(mPreviewSurface, mImageReaderSurface));

// Start a capture session using our open camera and list of Surfaces where frames will go
mSession = createCaptureSession(mCamera, targets, mCameraHandler);

try {
    CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
    requestBuilder.addTarget(mPreviewSurface);

    // This will keep sending the capture request as frequently as possible until the
    // session is torn down or session.stopRepeating() is called
    mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler);
} catch (CameraAccessException e) {
    e.printStackTrace();
}

觸發預拍序列並等待

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

Kotlin

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()
    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()
}

Java

private void runPrecaptureSequence() {
    // Creates a new capture request with CONTROL_AE_PRECAPTURE_TRIGGER_START
    try {
        CaptureRequest.Builder requestBuilder =
                mSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        requestBuilder.addTarget(mPreviewSurface);
        requestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);

        CountDownLatch precaptureLatch = new CountDownLatch(1);
        mSession.capture(requestBuilder.build(), new CameraCaptureSession.CaptureCallback() {
            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                           @NonNull CaptureRequest request,
                                           @NonNull TotalCaptureResult result) {
                Log.d(TAG, "CONTROL_AE_PRECAPTURE_TRIGGER_START processed");
                // Waiting for this callback ensures the precapture request has been processed
                precaptureLatch.countDown();
            }
        }, mCameraHandler);

        precaptureLatch.await();

        // Precapture trigger request has been processed, we can wait for AE & AWB convergence now
        mRepeatingCaptureCallback.awaitAeAwbConvergence();
    } catch (CameraAccessException | InterruptedException e) {
        e.printStackTrace();
    }
}

將所有內容拼接在一起

所有主要元件都已就緒,因此只要需要拍照,例如使用者按下快門按鈕拍照時,所有步驟都會按照先前的討論和程式碼範例所述的順序執行。

Kotlin

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

    // Perform I/O heavy operations in a different scope
    lifecycleScope.launch(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()
        v.post {
            // Clear the screen flash related UI changes
            restoreScreenBrightness()
            whiteColorOverlayView.visibility = View.INVISIBLE
        }
    }
}

Java

// User clicks captureButton to take picture
mCaptureButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Apply the screen flash related UI changes
        mWhiteColorOverlayView.setVisibility(View.VISIBLE);
        maximizeScreenBrightness();

        // Perform heavy operations in a different thread
        Executors.newSingleThreadExecutor().execute(() -> {
            // 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();

            v.post(() -> {
                // Clear the screen flash related UI changes
                restoreScreenBrightness();
                mWhiteColorOverlayView.setVisibility(View.INVISIBLE);
            });
        });
    }
});

圖片範例

您可以從以下範例瞭解螢幕閃爍效果實作錯誤和正確的情況。

錯誤執行時

如果未正確實作螢幕閃光功能,在多個擷取畫面、裝置和光線條件下,您會得到不一致的結果。拍攝的圖片通常會出現曝光不足或色調不正確的問題。對於某些裝置而言,這類錯誤在特定光線條件下會更明顯,例如光線不足的環境,而非完全黑暗的環境。

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

環境 曝光不足 過度曝光 色調
昏暗環境 (除了手機之外,沒有其他光源) 幾乎全黑的相片 過度曝光的相片 帶有紫色調的相片
低光源 (額外約 3 勒克斯的光源) 略為昏暗的相片 過度曝光的相片 帶有藍色調的照片

正確執行時

當標準導入方式用於相同的裝置和條件時,您可以在下表中查看結果。

環境 曝光不足 (已修正) 過度曝光 (已修正) 色調 (固定)
昏暗環境 (除了手機之外,沒有其他光源) 清晰的相片 清晰的相片 沒有任何色調的清晰相片
低光源 (額外提供約 3 勒克斯的光源) 清晰的相片 清晰的相片 沒有色調的清晰相片

如所觀察到的,標準導入方式可大幅改善圖片品質。