Neste codelab, vamos aprender a criar um app de câmera que usa a CameraX para mostrar um visor, tirar fotos e analisar um stream de imagem da câmera.
Para isso, vamos apresentar o conceito de casos de uso na CameraX, que pode ser usado em diversas operações de câmera, como mostrar um visor para analisar frames em tempo real.
O que vamos aprender
- Como adicionar as dependências da CameraX.
- Como mostrar a visualização da câmera em uma atividade. (Caso de uso da Preview)
- Como tirar uma foto e salvar no armazenamento. (Caso de uso da ImageCapture)
- Como analisar frames da câmera em tempo real. (Caso de uso da ImageAnalysis)
Hardware necessário
- Um dispositivo Android, embora o emulador do Android Studio funcione bem. O nível mínimo da API compatível é 21.
Software necessário
- Android Studio 3.3 ou versões mais recentes.
No menu do Android Studio, inicie um novo projeto e selecione Empty Activity quando solicitado.
Em seguida, podemos escolher qualquer nome. Escolhemos "CameraX App". Verifique se a linguagem está definida como Kotlin, o nível mínimo da API é 21 (o mínimo necessário para a CameraX) e se estamos usando artefatos do AndroidX.
Para começar, adicione as dependências da CameraX ao arquivo Gradle do app, na seção dependencies :
// Use the most recent version of CameraX, currently that is alpha04
def camerax_version = "1.0.0-alpha04"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
Quando solicitado, clique em Sincronizar agora para podermos usar a CameraX no nosso app.
Vamos usar uma SurfaceTexture para mostrar o visor da câmera. Neste codelab, vamos mostrar o visor em um formato quadrado de tamanho fixo. Para um exemplo mais abrangente que mostra um visor responsivo, confira o exemplo oficial.
Vamos editar o arquivo de layout activity_main em res > layout > activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextureView
android:id="@+id/view_finder"
android:layout_width="640px"
android:layout_height="640px"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Uma parte crucial da adição de qualquer funcionalidade ao nosso projeto que usa a câmera é solicitar as permissões CAMERA adequadas. Primeiro, precisamos declará-las no manifesto, antes da tag "Application":
<uses-permission android:name="android.permission.CAMERA" />
Em seguida, dentro da MainActivity, precisamos solicitar permissões durante a execução. Vamos fazer as mudanças no arquivo MainActivity em java > com.example.cameraxapp > MainActivity.kt:
Na parte de cima do arquivo, fora da definição da classe MainActivity, adicione as seguintes constantes e importações:
// Your IDE likely can auto-import these classes, but there are several
// different implementations so we list them here to disambiguate
import android.Manifest
import android.util.Size
import android.graphics.Matrix
import java.util.concurrent.TimeUnit
// This is an arbitrary number we are using to keep tab of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts
private const val REQUEST_CODE_PERMISSIONS = 10
// This is an array of all the permission specified in the manifest
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
Na classe MainActivity, adicione os seguintes campos e métodos auxiliares, que são usados para solicitar permissões e acionar nosso código quando soubermos que todas as permissões foram concedidas:
class MainActivity : AppCompatActivity(), LifecycleOwner {
override fun onCreate(savedInstanceState: Bundle?) {
...
}
// Add this after onCreate
private lateinit var viewFinder: TextureView
private fun startCamera() {
// TODO: Implement CameraX operations
}
private fun updateTransform() {
// TODO: Implement camera viewfinder transformations
}
/**
* Process result from permission request dialog box, has the request
* been granted? If yes, start Camera. Otherwise display a toast
*/
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
viewFinder.post { startCamera() }
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
/**
* Check if all permission specified in the manifest have been granted
*/
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
}
Por fim, juntamos tudo em onCreate para acionar a solicitação de permissão quando apropriado:
override fun onCreate(savedInstanceState: Bundle?) {
...
// Add this at the end of onCreate function
viewFinder = findViewById(R.id.view_finder)
// Request camera permissions
if (allPermissionsGranted()) {
viewFinder.post { startCamera() }
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// Every time the provided texture view changes, recompute layout
viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateTransform()
}
}
Agora, quando o aplicativo for iniciado, ele vai verificar se tem as permissões de câmera adequadas. Se for, ele vai chamar `startCamera()` diretamente. Caso contrário, ele vai solicitar as permissões e, depois de concedidas, chamar `startCamera()`.
Para a maioria dos aplicativos de câmera, mostrar um visor aos usuários é muito importante. Caso contrário, é muito difícil para eles apontarem a câmera para o lugar certo. Um visor pode ser implementado usando a classe "Preview" do CameraX.
Para usar a prévia, primeiro precisamos definir uma configuração que será usada para criar uma instância do caso de uso. A instância resultante é o que precisamos vincular ao ciclo de vida da CameraX. Vamos fazer isso no método `startCamera()`. Preencha a implementação com este código:
private fun startCamera() {
// Create configuration object for the viewfinder use case
val previewConfig = PreviewConfig.Builder().apply {
setTargetAspectRatio(Rational(1, 1))
setTargetResolution(Size(640, 640))
}.build()
// Build the viewfinder use case
val preview = Preview(previewConfig)
// Every time the viewfinder is updated, recompute layout
preview.setOnPreviewOutputUpdateListener {
// To update the SurfaceTexture, we have to remove it and re-add it
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
updateTransform()
}
// Bind use cases to lifecycle
// If Android Studio complains about "this" being not a LifecycleOwner
// try rebuilding the project or updating the appcompat dependency to
// version 1.1.0 or higher.
CameraX.bindToLifecycle(this, preview)
}
Neste ponto, precisamos implementar o misterioso método "updateTransform()". Dentro de `updateTransform()`, o objetivo é compensar as mudanças na orientação do dispositivo para mostrar o visor na rotação vertical:
private fun updateTransform() {
val matrix = Matrix()
// Compute the center of the view finder
val centerX = viewFinder.width / 2f
val centerY = viewFinder.height / 2f
// Correct preview output to account for display rotation
val rotationDegrees = when(viewFinder.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> return
}
matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
// Finally, apply transformations to our TextureView
viewFinder.setTransform(matrix)
}
Para implementar um app pronto para produção, confira a amostra oficial e saiba o que mais precisa ser processado. Para manter este codelab curto, vamos usar alguns atalhos. Por exemplo, não estamos rastreando algumas mudanças de configuração, como rotações de 180 graus do dispositivo, que não acionam nosso listener de mudança de layout. Os visores não quadrados também precisam compensar a mudança na proporção quando o dispositivo é girado.
Se criarmos e executarmos o app, vamos ver uma prévia em tempo real. Legal!
Para permitir que os usuários capturem imagens, vamos fornecer um botão como parte do layout após a visualização de textura em res > layout > activity_main.xml:
<ImageButton
android:id="@+id/capture_button"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_margin="24dp"
app:srcCompat="@android:drawable/ic_menu_camera"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
Outros casos de uso funcionam de maneira muito semelhante à prévia. Primeiro, definimos um objeto de configuração usado para instanciar o objeto do caso de uso real. Para capturar fotos, quando o botão de captura é pressionado, precisamos atualizar o método "startCamera()" e adicionar mais algumas linhas de código no final, antes da chamada para CameraX.bindToLifecycle:
private fun startCamera() {
...
// Add this before CameraX.bindToLifecycle
// Create configuration object for the image capture use case
val imageCaptureConfig = ImageCaptureConfig.Builder()
.apply {
setTargetAspectRatio(Rational(1, 1))
// We don't set a resolution for image capture; instead, we
// select a capture mode which will infer the appropriate
// resolution based on aspect ration and requested mode
setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
}.build()
// Build the image capture use case and attach button click listener
val imageCapture = ImageCapture(imageCaptureConfig)
findViewById<ImageButton>(R.id.capture_button).setOnClickListener {
val file = File(externalMediaDirs.first(),
"${System.currentTimeMillis()}.jpg")
imageCapture.takePicture(file,
object : ImageCapture.OnImageSavedListener {
override fun onError(error: ImageCapture.UseCaseError,
message: String, exc: Throwable?) {
val msg = "Photo capture failed: $message"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.e("CameraXApp", msg)
exc?.printStackTrace()
}
override fun onImageSaved(file: File) {
val msg = "Photo capture succeeded: ${file.absolutePath}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d("CameraXApp", msg)
}
})
}
// Bind use cases to lifecycle
// If Android Studio complains about "this" being not a LifecycleOwner
// try rebuilding the project or updating the appcompat dependency to
// version 1.1.0 or higher.
CameraX.bindToLifecycle(this, preview)
}
Em seguida, atualize a chamada para CameraX.bindToLifecycle para incluir o novo caso de uso:
CameraX.bindToLifecycle(this, preview, imageCapture)
E assim, implementamos um botão funcional para tirar fotos.
Um recurso muito interessante do CameraX é a classe ImageAnalysis. Ele permite definir uma classe personalizada que implementa a interface ImageAnalysis.Analyzer, que será chamada com os frames da câmera recebidos. De acordo com a visão principal do CameraX, não precisamos nos preocupar em gerenciar o estado da sessão da câmera nem descartar imagens. A vinculação ao ciclo de vida desejado do app é suficiente, como acontece com outros componentes com reconhecimento do ciclo de vida.
Primeiro, vamos implementar um analisador de imagens personalizado. Nosso analisador é bem simples. Ele apenas registra a média de luma (luminosidade) da imagem, mas exemplifica o que precisa ser feito para casos de uso arbitrariamente complexos. Basta substituir a função "analyze" em uma classe que implementa a interface ImageAnalysis.Analyzer. Podemos definir nossa implementação como uma classe interna na MainActivity:
private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
/**
* Helper extension function used to extract a byte array from an
* image plane buffer
*/
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
val currentTimestamp = System.currentTimeMillis()
// Calculate the average luma no more often than every second
if (currentTimestamp - lastAnalyzedTimestamp >=
TimeUnit.SECONDS.toMillis(1)) {
// Since format in ImageAnalysis is YUV, image.planes[0]
// contains the Y (luminance) plane
val buffer = image.planes[0].buffer
// Extract image data from callback object
val data = buffer.toByteArray()
// Convert the data into an array of pixel values
val pixels = data.map { it.toInt() and 0xFF }
// Compute average luminance for the image
val luma = pixels.average()
// Log the new luma value
Log.d("CameraXApp", "Average luminosity: $luma")
// Update timestamp of last analyzed frame
lastAnalyzedTimestamp = currentTimestamp
}
}
}
Com nossa classe implementando a interface ImageAnalysis.Analyzer, basta instanciar o ImageAnalysis como todos os outros casos de uso e atualizar a função "startCamera()" mais uma vez, antes da chamada para CameraX.bindToLifecycle:
private fun startCamera() {
...
// Add this before CameraX.bindToLifecycle
// Setup image analysis pipeline that computes average pixel luminance
val analyzerConfig = ImageAnalysisConfig.Builder().apply {
// Use a worker thread for image analysis to prevent glitches
val analyzerThread = HandlerThread(
"LuminosityAnalysis").apply { start() }
setCallbackHandler(Handler(analyzerThread.looper))
// In our analysis, we care more about the latest image than
// analyzing *every* image
setImageReaderMode(
ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
}.build()
// Build the image analysis use case and instantiate our analyzer
val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
analyzer = LuminosityAnalyzer()
}
// Bind use cases to lifecycle
// If Android Studio complains about "this" being not a LifecycleOwner
// try rebuilding the project or updating the appcompat dependency to
// version 1.1.0 or higher.
CameraX.bindToLifecycle(this, preview, imageCapture)
}
Também atualizamos a chamada para CameraX.bindtoLifecycle para vincular o novo caso de uso:
CameraX.bindToLifecycle(
this, preview, imageCapture, analyzerUseCase)
A execução do app agora vai produzir uma mensagem no logcat semelhante a esta aproximadamente a cada segundo:
D/CameraXApp: Average luminosity: ...
Para testar o app, basta clicar no botão Executar no Android Studio. O projeto será criado, implantado e iniciado no dispositivo ou emulador selecionado. Depois que o app for carregado, vamos ver o visor, que vai permanecer na vertical mesmo depois de girar o dispositivo graças ao código de processamento de orientação que adicionamos antes. Também será possível tirar fotos usando o botão:
Você concluiu o codelab com sucesso. Você implementou o seguinte em um novo app Android do zero:
- Incluiu dependências da CameraX no projeto.
- Mostrou um visor da câmera (usando o caso de uso de visualização)
- Implementou a captura de fotos e o salvamento de imagens no armazenamento (usando o caso de uso da ImageCapture).
- Implementou a análise de frames da câmera em tempo real (usando o caso de uso da ImageAnalysis).
Para saber mais sobre a CameraX e o que você pode fazer com ela, confira a documentação ou clone o exemplo oficial (links em inglês).