Olá! É muito bom ter você de volta na nossa série sobre a CameraX e o Jetpack Compose. Nos posts anteriores, abordamos os fundamentos da configuração de uma visualização da câmera e adicionamos a funcionalidade de tocar para focar.
🧱 Parte 1: como criar uma visualização básica da câmera usando o novo artefato camera-compose. Abordamos o processamento de permissões e a integração básica.
👆 Parte 2: como usar o sistema de gestos, gráficos e corrotinas do Compose para implementar a funcionalidade visual de tocar para focar.
🔦 Parte 3 (este post) : como sobrepor elementos da interface do Compose na parte de cima da visualização da câmera para uma experiência do usuário melhor.
📂 Parte 4: como usar APIs adaptáveis e o framework de animação do Compose para criar animações suaves ao entrar no modo de mesa e sair dele em smartphones dobráveis.
Neste post, vamos abordar algo um pouco mais envolvente em relação ao visual: a implementação de um efeito de destaque na parte de cima da visualização da câmera, usando a detecção de rosto como base para o efeito. Por quê? Não tenho uma resposta exata. Mas fica muito legal 🙂. E, mais importante, demonstra como podemos converter coordenadas de sensores em coordenadas de interface para usá-las no Compose.
Ativar a detecção facial
Primeiro, vamos modificar o CameraPreviewViewModel para ativar a detecção facial. Vamos usar a API Camera2Interop, que permite interagir com a API Camera2 subjacente da CameraX. Isso nos dá a oportunidade de usar recursos da câmera que não são expostos diretamente pela CameraX. Precisamos fazer as seguintes mudanças:
- Criar um StateFlow que contenha os limites faciais como uma lista de
Rects. - Definir a opção de solicitação de captura
STATISTICS_FACE_DETECT_MODEcomo FULL, que ativa a detecção facial. - Definir um
CaptureCallbackpara receber as informações faciais do resultado da captura.
class CameraPreviewViewModel : ViewModel() { ... private val _sensorFaceRects = MutableStateFlow(listOf<Rect>()) val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow() private val cameraPreviewUseCase = Preview.Builder() .apply { Camera2Interop.Extender(this) .setCaptureRequestOption( CaptureRequest.STATISTICS_FACE_DETECT_MODE, CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL ) .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) result.get(CaptureResult.STATISTICS_FACES) ?.map { face -> face.bounds.toComposeRect() } ?.toList() ?.let { faces -> _sensorFaceRects.update { faces } } } }) } .build().apply { ... }
Com essas mudanças, nosso modelo de visualização agora emite uma lista de Rect objetos que representam as caixas delimitadoras de rostos detectados em coordenadas de sensor.
Converter coordenadas de sensor em coordenadas de interface
As caixas delimitadoras de rostos detectados que armazenamos na seção anterior usam coordenadas no sistema de coordenadas do sensor. Para desenhar as caixas delimitadoras na interface, precisamos transformar essas coordenadas para que elas estejam corretas no sistema de coordenadas do Compose. Precisamos:
- Transformar as coordenadas do sensor em coordenadas do buffer de visualização.
- Transformar as coordenadas do buffer de visualização em coordenadas da interface do Compose.
Essas transformações são feitas usando matrizes de transformação. Cada uma das transformações tem sua própria matriz:
- Nosso
SurfaceRequestcontém uma instânciaTransformationInfo, que contém uma matrizsensorToBufferTranform. - Nosso
CameraXViewfindertem umCoordinateTransformerassociado. Você deve se lembrar de que já usamos esse transformador no post anterior do blog para transformar as coordenadas de tocar para focar.
Podemos criar um método auxiliar que faça a transformação para nós:
private fun List<Rect>.transformToUiCoords( transformationInfo: SurfaceRequest.TransformationInfo?, uiToBufferCoordinateTransformer: MutableCoordinateTransformer ): List<Rect> = this.map { sensorRect -> val bufferToUiTransformMatrix = Matrix().apply { setFrom(uiToBufferCoordinateTransformer.transformMatrix) invert() } val sensorToBufferTransformMatrix = Matrix().apply { transformationInfo?.let { setFrom(it.sensorToBufferTransform) } } val bufferRect = sensorToBufferTransformMatrix.map(sensorRect) val uiRect = bufferToUiTransformMatrix.map(bufferRect) uiRect }
- Iteramos a lista de rostos detectados e, para cada rosto, executamos a transformação.
- O
CoordinateTransformer.transformMatrixque recebemos do nossoCameraXViewfindertransforma as coordenadas da interface em coordenadas de buffer por padrão. No nosso caso, queremos que a matriz funcione de outra forma, transformando as coordenadas do buffer em coordenadas da interface. Portanto, usamos o métodoinvert()para inverter a matriz. - Primeiro, transformamos o rosto das coordenadas do sensor em coordenadas de buffer usando a
sensorToBufferTransformMatrixe, em seguida, transformamos essas coordenadas de buffer em coordenadas da interface usando abufferToUiTransformMatrix.
Implementar o efeito de destaque
Agora, vamos atualizar o elemento combinável CameraPreviewContent para desenhar o efeito de destaque. Vamos usar um Canvas combinável para desenhar uma máscara de gradiente na visualização, tornando os rostos detectados visíveis:
@Composable fun CameraPreviewContent( viewModel: CameraPreviewViewModel, modifier: Modifier = Modifier, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle() val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle() val transformationInfo by produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) { try { surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo -> value = transformationInfo } awaitCancellation() } finally { surfaceRequest?.clearTransformationInfoListener() } } val shouldSpotlightFaces by remember { derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null} } val spotlightColor = Color(0xDDE60991) .. surfaceRequest?.let { request -> val coordinateTransformer = remember { MutableCoordinateTransformer() } CameraXViewfinder( surfaceRequest = request, coordinateTransformer = coordinateTransformer, modifier = .. ) AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) { Canvas(Modifier.fillMaxSize()) { val uiFaceRects = sensorFaceRects.transformToUiCoords( transformationInfo = transformationInfo, uiToBufferCoordinateTransformer = coordinateTransformer ) // Fill the whole space with the color drawRect(spotlightColor) // Then extract each face and make it transparent uiFaceRects.forEach { faceRect -> drawRect( Brush.radialGradient( 0.4f to Color.Black, 1f to Color.Transparent, center = faceRect.center, radius = faceRect.minDimension * 2f, ), blendMode = BlendMode.DstOut ) } } } } }
Veja como funciona:
- Coletamos a lista de rostos do modelo de visualização.
- Para garantir que não estamos recompondo a tela inteira sempre que a lista de rostos detectados muda, usamos
derivedStateOfpara acompanhar se algum rosto é detectado. Isso pode ser usado comAnimatedVisibilitypara animar a sobreposição colorida. - O
surfaceRequestcontém as informações necessárias para transformar as coordenadas do sensor em coordenadas de buffer noSurfaceRequest.TransformationInfo. Usamos a funçãoproduceStatepara configurar um listener na solicitação de superfície e limpar esse listener quando o elemento combinável sair da árvore de composição. - Usamos um
Canvaspara desenhar um retângulo rosa translúcido que cobre a tela inteira. - Adiamos a leitura da variável
sensorFaceRectsaté estarmos dentro do bloco de desenhoCanvas. Em seguida, transformamos as coordenadas em coordenadas da interface. - Iteramos os rostos detectados e, para cada rosto, desenhamos um gradiente radial que tornará o interior do retângulo facial transparente.
- Usamos
BlendMode.DstOutpara garantir que estamos cortando o gradiente do retângulo rosa, criando o efeito de destaque.
Observação: quando você muda a câmera para DEFAULT_FRONT_CAMERA você notará que o destaque é espelhado. Esse é um problema conhecido, rastreado no Issue Tracker do Google.
Resultado
Com esse código, temos um efeito de destaque totalmente funcional que destaca os rostos detectados. Confira o snippet de código completo aqui.
Esse efeito é apenas o começo. Ao usar o poder do Compose, você pode criar inúmeras experiências de câmera visualmente impressionantes. A capacidade de transformar coordenadas de sensor e buffer em coordenadas da interface do Compose e vice-versa significa que podemos usar todos os recursos da interface do Compose e integrá-los perfeitamente ao sistema de câmera subjacente. Com animações, gráficos avançados da interface, gerenciamento de estado simples da interface e controle total de gestos, sua imaginação é o limite.
No post final da série, vamos mostrar como usar APIs adaptáveis e o framework de animação do Compose para fazer a transição perfeita entre diferentes interfaces de câmera em dispositivos dobráveis. Não perca!
Os snippets de código neste blog têm a seguinte licença:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
Agradecemos a Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner e Lauren Ward pela revisão e feedback. Tudo isso foi possível graças ao trabalho árduo de Yasith Vidanaarachch.
Continuar lendo
-
Como fazer
Neste artigo, você vai aprender a usar a API de teste waitUntil no Compose para aguardar o cumprimento de algumas condições.
Jose Alcérreca • Leitura de 3 minutos
-
Como fazer
Embora o desempenho do app seja frequentemente associado a uma interface suave e tempos de inicialização rápidos, a memória serve como a base silenciosa em que essas métricas visíveis são criadas. Não é segredo que estamos vendo uma mudança em que a memória do dispositivo é mais importante do que nunca.
Alice Yuan, Ajesh Pai, Fung Lam • Leitura de 10 minutos
-
Como fazer
Hoje, temos o prazer de anunciar uma nova credencial de e-mail verificada emitida pelo Google, que os desenvolvedores agora podem recuperar diretamente da API Digital Credential do Credential Manager do Android.
Niharika Arora, Jean-Pierre Pralle • Leitura de 3 minutos
Fique por dentro
Receba os insights mais recentes sobre o desenvolvimento do Android na sua caixa de entrada semanalmente.