화면 플래시

전면 플래시 또는 셀카 플래시라고도 하는 화면 플래시는 조명이 어두운 환경에서 전면 카메라로 이미지를 캡처할 때 휴대전화의 화면 밝기를 사용하여 피사체를 밝힙니다. 여러 네이티브 카메라 앱과 소셜 미디어 앱에서 사용할 수 있습니다. 대부분의 사람들이 셀카를 프레이밍할 때 휴대전화를 충분히 가까이 들고 있기 때문에 이 접근 방식이 효과적입니다.

그러나 개발자가 기능을 제대로 구현하고 여러 기기에서 좋은 캡처 품질을 일관되게 유지하기는 어렵습니다. 이 가이드에서는 하위 수준의 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 앱에서 화면 밝기를 변경하는 방법에는 여러 가지가 있습니다. 한 가지 직접적인 방법은 활동 창 참조에서 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();
        }
    }
}

사전 캡처 시퀀스 트리거

사전 캡처 측정 시퀀스를 트리거하려면 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)
    }
}

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럭스 광원) 사진 지우기 사진 지우기 색 번짐이 없는 선명한 사진

관찰된 바와 같이 이미지 품질은 표준 구현을 통해 크게 개선됩니다.