Olá! É muito bom ter você de volta na nossa série sobre o 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 prévia 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:usar o sistema de gestos, gráficos e corrotinas do Compose para implementar um toque para focar visual.
🔦 Parte 3 (esta postagem): como sobrepor elementos da interface do Compose na visualização da câmera para uma experiência do usuário mais rica.
📂 Parte 4: como usar APIs adaptáveis e o framework de animação do Compose para animar suavemente para e do modo de mesa em smartphones dobráveis.
Nesta postagem, vamos abordar algo um pouco mais interessante visualmente: a implementação de um efeito de destaque na parte de cima da prévia 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 de rosto. 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:
- Crie um StateFlow que contenha os limites do rosto como uma lista de
Rects. - Defina a opção de solicitação de captura
STATISTICS_FACE_DETECT_MODEcomo "FULL", o que ativa a detecção de rostos. - Defina um
CaptureCallbackpara receber as informações do rosto 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 objetos Rect que representam as caixas delimitadoras dos rostos detectados nas coordenadas do sensor.
Traduzir coordenadas do sensor para coordenadas da interface
As caixas delimitadoras dos rostos detectados que armazenamos na última seção usam coordenadas no sistema de coordenadas do sensor. Para desenhar as caixas delimitadoras na nossa 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 prévia
- 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 uma matriz própria:
- Nosso
SurfaceRequestmantém uma instânciaTransformationInfo, que contém uma matrizsensorToBufferTranform. - Nosso
CameraXViewfindertem umCoordinateTransformerassociado. Você deve se lembrar que já usamos esse transformador na postagem anterior do blog para transformar coordenadas de toque para foco.
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 executamos a transformação para cada um deles.
- O
CoordinateTransformer.transformMatrixque recebemos do nossoCameraXViewfindertransforma as coordenadas da interface em coordenadas do buffer por padrão. No nosso caso, queremos que a matriz funcione ao contrário, transformando coordenadas de buffer em coordenadas de UI. Portanto, usamos o métodoinvert()para inverter a matriz. - Primeiro, transformamos o rosto de coordenadas do sensor em coordenadas de buffer usando o
sensorToBufferTransformMatrixe, em seguida, transformamos essas coordenadas de buffer em coordenadas de interface usando obufferToUiTransformMatrix.
Implementar o efeito de destaque
Agora, vamos atualizar o elemento combinável CameraPreviewContent para desenhar o efeito de destaque. Vamos usar um elemento combinável Canvas para desenhar uma máscara de gradiente sobre a prévia, 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 vamos recompor a tela inteira sempre que a lista de rostos detectados mudar, usamos
derivedStateOfpara acompanhar se algum rosto foi 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 sai da árvore de composição. - Usamos um
Canvaspara desenhar um retângulo rosa translúcido que cobre toda a tela. - Adiamos a leitura da variável
sensorFaceRectsaté estarmos dentro do bloco de desenhoCanvas. Em seguida, transformamos as coordenadas em coordenadas da interface. - Iteramos sobre os rostos detectados e, para cada um deles, desenhamos um gradiente radial que torna o interior do retângulo 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, 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 de interface, gerenciamento simples de estado da interface e controle total de gestos, sua imaginação é o limite.
Na postagem final da série, vamos explicar como usar APIs adaptáveis e o framework de animação do Compose para fazer transições perfeitas 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 por revisarem e enviarem feedback. Possível graças ao trabalho 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 • 3 min de leitura
-
Tutoriais
A fiscalização da qualidade técnica da bateria chegou: como otimizar casos de uso comuns de WakeLock
Sabendo 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
-
Tutoriais
Queremos mostrar exemplos de recursos com tecnologia de IA usando modelos no dispositivo e na nuvem para inspirar você a criar experiências incríveis para seus usuários.
Thomas Ezan, Ivy Knight • Leitura de 2 minutos
Fique por dentro
Receba os insights mais recentes sobre desenvolvimento Android na sua caixa de entrada semanalmente.