Aufleuchten des Bildschirms bei Benachrichtigungen

Beim Aufleuchten bei Benachrichtigungen, auch Frontblitz oder Selfie-Blitz genannt, wird die Bildschirmhelligkeit eines Smartphones genutzt, um das Motiv zu beleuchten, wenn Bilder mit der Frontkamera bei schlechten Lichtverhältnissen aufgenommen werden. Sie ist in vielen nativen Kamera-Apps und Social-Media-Apps verfügbar. Da die meisten Menschen ihr Smartphone beim Aufnehmen eines Selfies nah genug halten, ist dieser Ansatz effektiv.

Es ist jedoch schwierig für Entwickler, die Funktion richtig zu implementieren und auf allen Geräten eine gleichbleibend gute Aufnahmequalität zu gewährleisten. In diesem Leitfaden wird beschrieben, wie du diese Funktion mit Camera2, der Low-Level-API des Android-Kamera-Frameworks, richtig implementierst.

Allgemeiner Workflow

Für die ordnungsgemäße Implementierung der Funktion sind zwei Faktoren entscheidend: die Verwendung der Messsequenz vor der Aufnahme (automatische Belichtung vor der Aufnahme) und das Timing der Vorgänge. Der allgemeine Workflow ist in Abbildung 1 dargestellt.

Flussdiagramm, das zeigt, wie eine Benutzeroberfläche für das Aufleuchten bei Benachrichtigungen in Camera2 verwendet wird.
Abbildung 1. Allgemeiner Workflow für die Implementierung von „Aufleuchten bei Benachrichtigungen“.

Die folgenden Schritte sind erforderlich, wenn ein Bild mit der Funktion „Aufleuchten bei Benachrichtigungen“ aufgenommen werden soll.

  1. Wende die für das Aufleuchten bei Benachrichtigungen erforderlichen UI-Änderungen an, damit der Gerätebildschirm ausreichend Licht für die Aufnahme von Fotos bietet. Für allgemeine Anwendungsfälle empfiehlt Google die folgenden Änderungen an der Benutzeroberfläche, die auch in unseren Tests verwendet wurden:
    • Der App-Bildschirm ist mit einem weißen Farb-Overlay bedeckt.
    • Die Bildschirmhelligkeit ist auf das Maximum eingestellt.
  2. Stellen Sie den Modus für die automatische Belichtung (AE) auf CONTROL_AE_MODE_ON_EXTERNAL_FLASH ein, falls unterstützt.
  3. Löse mit CONTROL_AE_PRECAPTURE_TRIGGER eine Messsequenz vor der Aufnahme aus.
  4. Warte, bis die automatische Belichtung (AE) und der automatische Weißabgleich (AWB) konvergiert sind.

  5. Nach der Konvergenz wird der übliche Ablauf der Fotoaufnahme der App verwendet.

  6. Aufnahmeanfrage an das Framework senden.

  7. Warten Sie, bis das Ergebnis der Erfassung empfangen wurde.

  8. Setzt den AE-Modus zurück, wenn CONTROL_AE_MODE_ON_EXTERNAL_FLASH festgelegt wurde.

  9. Die Änderungen an der Benutzeroberfläche für das Aufleuchten bei Benachrichtigungen werden gelöscht.

Camera2-Beispielcodes

App-Bildschirm mit weißem Overlay abdecken

Fügen Sie der XML-Layoutdatei Ihrer Anwendung eine Ansicht hinzu. Die Ansicht hat eine ausreichende Höhe, um während der Aufnahme des Aufleuchtens bei Benachrichtigungen über allen anderen UI-Elementen zu liegen. Sie ist standardmäßig unsichtbar und wird nur sichtbar, wenn die Änderungen an der Benutzeroberfläche für das Aufleuchten bei Benachrichtigungen angewendet werden.

Im folgenden Codebeispiel wird die Farbe Weiß (#FFFFFF) als Beispiel für die Ansicht verwendet. Anwendungen können die Farbe auswählen oder Nutzern je nach Anforderungen mehrere Farben anbieten.

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

Maximale Displayhelligkeit

Es gibt mehrere Möglichkeiten, die Bildschirmhelligkeit in einer Android-App zu ändern. Eine direkte Möglichkeit besteht darin, den WindowManager-Parameter screenBrightness in der Referenz Activity Window zu ändern.

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

AE-Modus auf CONTROL_AE_MODE_ON_EXTERNAL_FLASH festlegen

CONTROL_AE_MODE_ON_EXTERNAL_FLASH ist ab API-Level 28 verfügbar. Dieser AE-Modus ist jedoch nicht auf allen Geräten verfügbar. Prüfe daher, ob der AE-Modus verfügbar ist, und lege den Wert entsprechend fest. Verwenden Sie CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES, um die Verfügbarkeit zu prüfen.

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

Wenn für die Anwendung eine wiederholte Aufnahme-Anfrage festgelegt ist (für die Vorschau erforderlich), muss der AE-Modus auf die wiederholte Anfrage festgelegt werden. Andernfalls wird er möglicherweise bei der nächsten wiederholten Aufnahme durch einen Standard- oder einen anderen vom Nutzer festgelegten AE-Modus überschrieben. In diesem Fall hat die Kamera möglicherweise nicht genügend Zeit, um alle Vorgänge auszuführen, die sie normalerweise für einen externen Blitz-AE-Modus ausführt.

Damit die Kamera die Anfrage zur Aktualisierung des AE-Modus vollständig verarbeitet, prüfe das Aufnahmeergebnis im Callback für die wiederholte Aufnahme und warte, bis der AE-Modus im Ergebnis aktualisiert wurde.

Callback für die Aufnahme, der auf die Aktualisierung des AE-Modus warten kann

Das folgende Code-Snippet zeigt, wie das geht.

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

Wiederkehrende Anfrage zum Aktivieren oder Deaktivieren des AE-Modus festlegen

Nachdem der Aufnahme-Callback eingerichtet wurde, zeigen die folgenden Codebeispiele, wie eine wiederholte Anfrage festgelegt wird.

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

Pre-Capture-Sequenz auslösen

Wenn du eine Messsequenz vor der Aufnahme auslösen möchtest, kannst du eine CaptureRequest mit dem Wert CONTROL_AE_PRECAPTURE_TRIGGER_START an die Anfrage senden. Sie müssen warten, bis die Anfrage verarbeitet wurde und die AE- und AWB-Werte konvergieren.

Obwohl die Voraufnahme mit einer einzelnen Aufnahmeanfrage ausgelöst wird, ist das Warten auf die AE- und AWB-Konvergenz komplexer. Sie können den AE-Status und den AWB-Status mithilfe eines Aufnahme-Callbacks verfolgen, der auf eine wiederholte Anfrage festgelegt ist.

Wenn Sie denselben wiederkehrenden Callback aktualisieren, bleibt der Code einfach. Anwendungen erfordern oft eine Vorschau, für die sie beim Einrichten der Kamera eine wiederholte Anfrage einrichten. Sie können den Callback für die wiederholte Aufnahme einmal auf diese erste wiederholte Anfrage festlegen und ihn dann für die Ergebnisprüfung und das Warten wiederverwenden.

Callback-Code-Update erfassen, um auf Konvergenz zu warten

Verwenden Sie das folgende Code-Snippet, um den Callback für die wiederholte Aufnahme zu aktualisieren.

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

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

Rückruf für eine wiederholte Anfrage während der Kameraeinrichtung festlegen

Mit dem folgenden Codebeispiel können Sie den Callback während der Initialisierung auf eine wiederholte Anfrage festlegen.

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

Auslösen und Warten auf die Precapture-Sequenz

Wenn der Callback festgelegt ist, können Sie das folgende Codebeispiel verwenden, um eine Pre-Capture-Sequenz auszulösen und darauf zu warten.

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

Alles zusammenfügen

Wenn alle wichtigen Komponenten bereit sind, können alle Schritte in der Reihenfolge ausgeführt werden, die in der vorherigen Erläuterung und in den Codebeispielen beschrieben wurde, wenn ein Bild aufgenommen werden muss, z. B. wenn ein Nutzer auf die Aufnahmeschaltfläche klickt, um ein Bild aufzunehmen.

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

    // Launch on the main thread, then switch to IO for background operations.
    lifecycleScope.launch {
        // Perform I/O heavy operations in a different scope.
        withContext(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()
        }

        // Back on the main thread automatically.
        // Clear the screen flash related UI changes.
        restoreScreenBrightness()
        whiteColorOverlayView.visibility = View.INVISIBLE
    }
}

Beispielbilder

Die folgenden Beispiele zeigen, was passiert, wenn das Aufleuchten bei Benachrichtigungen falsch oder richtig implementiert wird.

Wenn sie falsch gemacht werden,

Wenn das Aufleuchten bei Benachrichtigungen nicht richtig implementiert ist, kann es bei mehreren Aufnahmen, Geräten und Lichtverhältnissen zu inkonsistenten Ergebnissen kommen. Oft haben aufgenommene Bilder ein Problem mit der Belichtung oder dem Farbton. Bei einigen Geräten werden diese Arten von Fehlern bei bestimmten Lichtverhältnissen deutlicher, z. B. bei wenig Licht statt bei völliger Dunkelheit.

In der folgenden Tabelle finden Sie Beispiele für solche Probleme. Sie werden in der CameraX-Lab-Infrastruktur aufgenommen, wobei die Lichtquellen auf einer warmweißen Farbe bleiben. Anhand dieser warmweißen Lichtquelle können Sie sehen, dass der blaue Farbstich ein tatsächliches Problem und kein Nebeneffekt einer Lichtquelle ist.

Umgebung Unterbelichtung Überbelichtung Farbton
Dunkle Umgebung (keine Lichtquelle außer dem Smartphone) Fast vollständig dunkles Foto Zu stark aufgehelltes Foto Foto mit lila Farbstich
Wenig Licht (zusätzliche Lichtquelle mit ca. 3 Lux) Etwas dunkles Foto Zu stark aufgehelltes Foto Foto mit bläulichem Farbton

Wenn sie richtig gemacht wird,

Wenn die Standardimplementierung für dieselben Geräte und Bedingungen verwendet wird, sehen Sie die Ergebnisse in der folgenden Tabelle.

Umgebung Unterbelichtung (behoben) Überbelichtung (behoben) Farbton (festgelegt)
Dunkle Umgebung (keine Lichtquelle außer dem Smartphone) Foto löschen Foto löschen Klares Foto ohne Farbstich
Wenig Licht (zusätzliche Lichtquelle mit ca. 3 Lux) Foto löschen Foto löschen Ein klares Foto ohne Farbstich

Wie Sie sehen, verbessert sich die Bildqualität mit der Standardimplementierung erheblich.