El flash de pantalla, también llamado flash frontal o flash para selfies, utiliza el brillo de la pantalla del teléfono para iluminar al sujeto cuando se capturan imágenes con la cámara frontal en condiciones de poca luz. Está disponible en muchas apps de cámara nativas y de redes sociales. Como la mayoría de las personas sostienen el teléfono lo suficientemente cerca cuando encuadran un autorretrato, este enfoque es eficaz.
Sin embargo, a los desarrolladores les resulta difícil implementar la función correctamente y mantener una buena calidad de captura de manera constante en todos los dispositivos. En esta guía, se muestra cómo implementar correctamente esta función con Camera2, la API de framework de cámara de Android de bajo nivel.
Flujo de trabajo general
Para implementar la función correctamente, los dos factores clave son el uso de la secuencia de medición previa a la captura (precaptura de exposición automática) y el tiempo de las operaciones. El flujo de trabajo general se muestra en la Figura 1.

Los siguientes pasos se utilizan cuando se necesita capturar una imagen con la función de flash de pantalla.
- Aplica los cambios en la IU necesarios para el flash de pantalla, que puede proporcionar suficiente luz para tomar fotos con la pantalla del dispositivo. Para los casos de uso generales, Google sugiere los siguientes cambios en la IU, como se usaron en nuestras pruebas:
- La pantalla de la app está cubierta con una superposición de color blanco.
- El brillo de la pantalla se maximiza.
- Establece el modo de exposición automática (AE) en
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
, si es compatible. - Activa una secuencia de medición previa a la captura con
CONTROL_AE_PRECAPTURE_TRIGGER
. Espera a que converjan la exposición automática (AE) y el balance de blancos automático (AWB).
Una vez que se logra la convergencia, se usa el flujo habitual de captura de fotos de la app.
Envía una solicitud de captura al framework.
Espera a recibir el resultado de la captura.
Restablece el modo AE si se configuró
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
.Borra los cambios de la IU para el flash de pantalla.
Códigos de muestra de Camera2
Cubrir la pantalla de la app con una capa superpuesta de color blanco
Agrega una View en el archivo XML de diseño de tu aplicación. La vista tiene la elevación suficiente para estar sobre todos los demás elementos de la IU durante la captura del flash de pantalla. Se mantiene invisible de forma predeterminada y solo se hace visible cuando se aplican los cambios en la IU del flash de la pantalla.
En la siguiente muestra de código, se usa el color blanco (#FFFFFF
) como ejemplo para la vista. Las aplicaciones pueden elegir el color u ofrecer varios colores a los usuarios, según sus requisitos.
<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" />
Maximizar el brillo de la pantalla
Existen varias formas de cambiar el brillo de la pantalla en una app para Android. Una forma directa es cambiar el parámetro screenBrightness de WindowManager en la referencia de ventana de actividad.
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); }
Establece el modo AE en CONTROL_AE_MODE_ON_EXTERNAL_FLASH
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
está disponible con el nivel de API 28 o versiones posteriores.
Sin embargo, este modo de AE no está disponible en todos los dispositivos, por lo que debes verificar si está disponible y establecer el valor según corresponda. Para verificar la disponibilidad, usa 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; }
Si la aplicación tiene establecida una solicitud de captura repetida (es obligatoria para la vista previa), el modo AE debe establecerse en la solicitud repetida. De lo contrario, es posible que se anule con un modo de AE predeterminado o establecido por el usuario en la siguiente captura repetida. Si esto sucede, es posible que la cámara no tenga suficiente tiempo para realizar todas las operaciones que normalmente hace para un modo AE de flash externo.
Para asegurarte de que la cámara procese por completo la solicitud de actualización del modo AE, verifica el resultado de la captura en la devolución de llamada de captura repetida y espera a que se actualice el modo AE en el resultado.
Devolución de llamada de captura que puede esperar a que se actualice el modo AE
En el siguiente fragmento de código, se muestra cómo lograrlo.
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();
Cómo configurar una solicitud repetida para habilitar o inhabilitar el modo AE
Con la devolución de llamada de captura implementada, las siguientes muestras de código muestran cómo establecer una solicitud repetida.
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(); } } }
Cómo activar una secuencia de precaptura
Para activar una secuencia de medición previa a la captura, puedes enviar un CaptureRequest
con el valor CONTROL_AE_PRECAPTURE_TRIGGER_START
establecido en la solicitud. Debes esperar a que se procese la solicitud y, luego, a que converjan el AE y el AWB.
Si bien la precaptura se activa con una sola solicitud de captura, esperar la convergencia de la AE y el AWB requiere más complejidad. Puedes hacer un seguimiento del estado de AE y el estado de AWB con una devolución de llamada de captura establecida en una solicitud repetida.
Actualizar la misma devolución de llamada repetida te permite tener un código simple. Las aplicaciones suelen requerir una vista previa para la que configuran una solicitud repetida mientras configuran la cámara. Por lo tanto, puedes establecer la devolución de llamada de captura repetida en esa solicitud repetida inicial una vez y, luego, volver a usarla para verificar los resultados y esperar.
Actualización del código de devolución de llamada de captura para esperar la convergencia
Para actualizar la devolución de llamada de captura repetida, usa el siguiente fragmento de código.
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); } }
Establece la devolución de llamada en una solicitud repetida durante la configuración de la cámara
El siguiente ejemplo de código te permite establecer la devolución de llamada en una solicitud repetida durante la inicialización.
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(); }
Activación y espera de la secuencia previa a la captura
Con la devolución de llamada establecida, puedes usar la siguiente muestra de código para activar y esperar una secuencia de precaptura.
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(); } }
Une todo
Con todos los componentes principales listos, cada vez que se necesite tomar una foto, como cuando un usuario hace clic en el botón de captura para tomar una foto, todos los pasos se pueden ejecutar en el orden indicado en el análisis y las muestras de código anteriores.
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); }); }); } });
Imágenes de muestra
En los siguientes ejemplos, puedes ver qué sucede cuando el flash de pantalla se implementa de forma incorrecta y cuando se implementa de forma correcta.
Cuando se hace de forma incorrecta
Si el flash de pantalla no se implementa correctamente, obtendrás resultados incoherentes en varias capturas, dispositivos y condiciones de iluminación. A menudo, las imágenes capturadas tienen problemas de exposición o de tono de color. En algunos dispositivos, este tipo de errores se hacen más evidentes en condiciones de iluminación específicas, como en un entorno con poca luz en lugar de uno completamente oscuro.
En la siguiente tabla, se muestran ejemplos de estos problemas. Se toman en la infraestructura de laboratorio de CameraX, con fuentes de luz que permanecen en un color blanco cálido. Esta fuente de luz blanca cálida te permite ver cómo el tinte de color azul es un problema real, no un efecto secundario de una fuente de luz.
Entorno | Subexposición | Sobreexposición | Tono de color |
---|---|---|---|
Entorno oscuro (sin fuente de luz, excepto el teléfono) |
![]() |
![]() |
![]() |
Poca luz (fuente de luz adicional de aproximadamente 3 lux) |
![]() |
![]() |
![]() |
Cuando se hace bien
Cuando se usa la implementación estándar para los mismos dispositivos y condiciones, puedes ver los resultados en la siguiente tabla.
Entorno | Subexposición (fija) | Sobreexposición (corregida) | Tono de color (fijo) |
---|---|---|---|
Entorno oscuro (sin fuente de luz, excepto el teléfono) |
![]() |
![]() |
![]() |
Poca luz (fuente de luz adicional de aproximadamente 3 lux) |
![]() |
![]() |
![]() |
Como se observa, la calidad de la imagen mejora significativamente con la implementación estándar.