螢幕閃光燈 (又稱前置閃光燈或自拍閃光燈) 會在低光源環境下使用前置鏡頭拍攝相片時,利用手機螢幕亮度照亮主體。許多原生相機應用程式和社群媒體應用程式都支援這項功能。大多數人在拍攝自拍時都會拿著手機靠近自己,因此這種做法很有效。
不過,開發人員很難正確實作這項功能,並在不同裝置上維持良好的擷取品質。本指南將說明如何使用 Camera2 (Android 低階攝影機架構 API),正確導入這項功能。
一般工作流程
如要正確實作這項功能,有兩個重要因素:使用預先擷取計量序列 (自動曝光預先擷取),以及作業的時機。一般工作流程如圖 1 所示。

如果需要使用螢幕閃光燈功能拍攝圖片,請按照下列步驟操作。
- 套用螢幕閃光燈所需的 UI 變更,以便使用裝置螢幕提供足夠的光線來拍照。針對一般用途,Google 建議進行下列 UI 變更 (如測試所用):
- 應用程式畫面覆蓋白色疊加層。
- 螢幕亮度調到最高。
- 將自動曝光 (AE) 模式設為
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
(如果支援)。 - 使用
CONTROL_AE_PRECAPTURE_TRIGGER
觸發預先擷取計量序列。 等待自動曝光 (AE) 和自動白平衡 (AWB) 收斂。
融合完成後,系統會使用應用程式的一般相片拍攝流程。
將擷取要求傳送至架構。
等待接收擷取結果。
如果已設定
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
,請重設 AE 模式。清除螢幕閃光燈的 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 應用程式中,變更螢幕亮度的方法有很多種。其中一種直接方法是變更 Activity Window 參照中的 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 匯聚。
雖然預先擷取會觸發單一擷取要求,但等待 AE 和 AWB 聚合需要更多複雜性。您可以透過設為重複要求的擷取回呼,追蹤 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 Listtargets = 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 勒克斯的光源) |
![]() |
![]() |
![]() |
如您所見,標準實作方式可大幅提升圖片品質。