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 pode fazer 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 de 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 sobre a 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 esse processo 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 de 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ê vai 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 uma infinidade de experiências de câmera visualmente impressionantes. A capacidade de transformar coordenadas de sensor e buffer em coordenadas de 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 simples do estado 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. Feito com o trabalho árduo de Yasith Vidanaarachch.
Continuar lendo
-
Tutoriais
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
-
Tutoriais
Se você estiver usando o Gemini no Android Studio, a CLI do Gemini, o Antigravity ou agentes de terceiros, como o Claude Code ou o Codex, nossa missão é garantir que o desenvolvimento de alta qualidade do Android seja possível em qualquer lugar.
Adarsh Fernando, Esteban de la Canal • Leitura de 4 minutos
-
Tutoriais
Reconhecendo que o consumo elevado da bateria é o mais lembrado para os usuários do Android, o Google tem tomado medidas significativas para ajudar os desenvolvedores a criar apps mais eficientes em termos de energia.
Alice Yuan • Leitura de 8 minutos
Fique por dentro
Receba os insights mais recentes sobre o desenvolvimento do Android na sua caixa de entrada semanalmente.