화면 플래시

전면 플래시 또는 셀카 플래시라고도 하는 화면 플래시는 저조도 환경에서 전면 카메라로 이미지를 캡처할 때 휴대전화의 화면 밝기를 활용하여 피사체를 밝힙니다. 이 기능은 많은 네이티브 카메라 앱과 소셜 미디어 앱에서 사용할 수 있습니다. 대부분의 사용자가 셀카를 찍을 때 휴대전화를 충분히 가까이 들고 있기 때문에 이 방법이 효과적입니다.

하지만 개발자가 이 기능을 제대로 구현하고 기기 전반에서 일관되게 우수한 캡처 품질을 유지하기는 어렵습니다. 이 가이드에서는 하위 수준 Android 카메라 프레임워크 API인 Camera2를 사용하여 이 기능을 올바르게 구현하는 방법을 보여줍니다.

일반 워크플로

이 기능을 올바르게 구현하려면 두 가지 주요 요소인 촬영 전 측정 시퀀스 (자동 노출 촬영 전)의 사용과 작업 시점이 중요합니다. 일반적인 워크플로는 그림 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 파일에 뷰를 추가합니다. 뷰는 화면 플래시 캡처 중에 다른 모든 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
        }
    }
}

자바

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

자바

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

자바

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

자바

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

사전 캡처 시퀀스 트리거

촬영 전 측정 시퀀스를 트리거하려면 CONTROL_AE_PRECAPTURE_TRIGGER_START 값을 요청에 설정하여 CaptureRequest를 제출하면 됩니다. 요청이 처리될 때까지 기다린 후 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)
    }
}

자바

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)

자바

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

자바

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

자바

// 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 실험실 인프라에서 촬영되었으며, 조명은 따뜻한 흰색으로 유지되었습니다. 이 따뜻한 백색광원에서는 파란색 색조가 광원의 부작용이 아니라 실제 문제임을 알 수 있습니다.

환경 노출 부족 과다 노출 색조
어두운 환경 (휴대전화 이외의 광원 없음) 거의 완전히 어두운 사진 지나치게 밝게 보정된 사진 보라색 색조가 있는 사진
저조도 (추가 약 3lux 광원) 약간 어두운 사진 지나치게 밝게 보정된 사진 푸른색 색조가 있는 사진

올바르게 수행하는 경우

동일한 기기와 조건에 표준 구현을 사용하면 다음 표에서 결과를 확인할 수 있습니다.

환경 노출 부족 (해결됨) 노출 오버 (해결됨) 색조 (고정)
어두운 환경 (휴대전화 이외의 광원 없음) 사진 지우기 사진 지우기 색조가 없는 선명한 사진
저조도 (추가 약 3lux 광원) 사진 지우기 사진 지우기 색조가 없는 선명한 사진

표준 구현을 사용하면 이미지 품질이 크게 개선되는 것을 확인할 수 있습니다.