Migrar a Camera1 para a CameraX

Caso o app use a classe Camera original ("Camera1"), que está descontinuada desde o Android 5.0 (nível 21 da API), é altamente recomendável atualizar para uma API moderna de câmera do Android. O Android oferece a CameraX, uma API de câmera do Jetpack padronizada e robusta, e a Camera2, uma API de framework de baixo nível. Na maioria dos casos, recomendamos migrar seu app para o CameraX. Conheça as vantagens:

  • Facilidade de uso: o CameraX processa os detalhes de baixo nível para que você possa se concentrar menos na criação de uma experiência de câmera do zero e mais em diferenciar o app.
  • O CameraX processa a fragmentação para você: o CameraX reduz os custos de manutenção de longo prazo e o código específico do dispositivo, trazendo experiências de maior qualidade para os usuários. Para saber mais, confira a postagem do blog Melhor compatibilidade de dispositivos com o CameraX.
  • Recursos avançados: o CameraX foi cuidadosamente projetado para facilitar a integração de funcionalidades avançadas ao app. Por exemplo, você pode aplicar facilmente o bokeh, o retoque facial, o HDR (High Dynamic Range) e o modo de captura noturna com pouca luz às suas fotos com as extensões do CameraX.
  • Capacidade de atualização: o Android lança novos recursos e correções de bugs para o CameraX ao longo do ano. Ao migrar para o CameraX, seu app recebe a tecnologia de câmera Android mais recente com cada versão do CameraX, não apenas nas versões anuais do Android.

Neste guia, você vai encontrar cenários comuns para apps de câmera. Cada cenário inclui uma implementação do Camera1 e do CameraX para comparação.

Às vezes é preciso mais flexibilidade para integrar com uma base de código existente na migração. Todo o código do CameraX neste guia tem uma implementação de CameraController, que é ótimo se você quer a maneira mais simples de usar o CameraX, e também uma implementação de CameraProvider, que é ótimo se você precisa de mais flexibilidade. Confira a seguir quais são os benefícios de cada uma para decidir qual é a mais adequada:

CameraController

CameraProvider

Requer pouco código de configuração Oferece mais controle
Permitir que o CameraX faça mais do processo de configuração significa que funcionalidades como o "toque para focar" e o gesto de pinça para aplicar zoom funcionam automaticamente Como o desenvolvedor de apps processa a configuração, há mais oportunidades para personalizar a configuração, como ativar a rotação da imagem de saída ou definir o formato da imagem de saída em ImageAnalysis.
A exigência de PreviewView para a visualização da câmera permite que o CameraX ofereça integração completa de ponta a ponta, como na nossa integração do Kit de ML, que pode mapear as coordenadas de resultado do modelo de ML (como caixas delimitadoras de rosto) diretamente nas coordenadas de visualização A capacidade de usar uma "superfície" personalizada para a visualização da câmera permite mais flexibilidade, como usar o código da "superfície" atual, que pode ser uma entrada para outras partes do app

Se você tiver dificuldades para migrar, entre em contato no grupo de discussão do CameraX.

Antes da migração

Comparar o CameraX com o Camera1

Embora o código possa parecer diferente, os conceitos subjacentes no Camera1 e no CameraX são muito parecidos. O CameraX abstrai a funcionalidade de câmera comum em casos de uso e, como resultado, muitas tarefas que foram deixadas para o desenvolvedor no Camera1 são processadas automaticamente pelo CameraX. Existem quatro UseCases no CameraX, que você pode usar para várias tarefas de câmera: Preview, ImageCapture, VideoCapture e ImageAnalysis.

Um exemplo do CameraX processando detalhes de baixo nível para desenvolvedores é a ViewPort que é compartilhada entre UseCases ativos. Isso garante que todos os UseCases vejam exatamente os mesmos pixels. No Camera1, você precisa gerenciar esses detalhes por conta própria e, devido à variabilidade nas proporções entre os sensores e as telas da câmera dos dispositivos, pode ser difícil garantir que a visualização corresponda às fotos e aos vídeos capturados.

Como outro exemplo, o CameraX processa callbacks de Lifecycle automaticamente na instância de Lifecycle transmitida. Isso significa que o CameraX processa a conexão do app com a câmera durante todo o ciclo de vida da atividade do Android, incluindo os seguintes casos: fechar a câmera quando o app entra no segundo plano, remover a visualização da câmera quando a tela não precisa mais de exibição e pausar a visualização da câmera quando outra atividade tem precedência, como uma videochamada recebida.

Por fim, o CameraX processa a rotação e o dimensionamento sem precisar de nenhum outro código. No caso de uma Activity com orientação desbloqueada, a configuração de UseCase é feita sempre que o dispositivo é girado, conforme o sistema destrói e recria a Activity nas mudanças de orientação. Isso faz com que UseCases definam a rotação desejada para corresponderem à orientação da tela por padrão sempre. Leia mais sobre rotações no CameraX.

Antes de passarmos para os detalhes, confira uma visão geral de UseCases do CameraX e como um app Camera1 se identificaria neles. Os conceitos do CameraX estão em azul e os conceitos do Camera1 estão em verde.

CameraX

Configuração do CameraController / CameraProvider
Prévia ImageCapture VideoCapture ImageAnalysis
Gerenciar a superfície de visualização e configurá-la na câmera Configurar PictureCallback e chamar takePicture() na câmera Gerenciar a configuração da câmera e do MediaRecorder em uma ordem específica Código de análise personalizado criado com base na plataforma de visualização
Código específico do dispositivo
Rotação do dispositivo e gerenciamento de dimensionamento
Gerenciamento de sessão da câmera (seleção da câmera, gerenciamento do ciclo de vida)

Camera1

Compatibilidade e desempenho no CameraX

O CameraX oferece suporte a dispositivos com o Android 5.0 (nível da API 21) ou mais recentes. Isso representa mais de 98% dos dispositivos Android existentes. O CameraX foi criado para processar diferenças automaticamente entre dispositivos, reduzindo a necessidade de códigos específicos do dispositivo no app. Além disso, testamos mais de 150 dispositivos físicos em todas as versões do Android desde a versão 5.0 no CameraX Test Lab. Confira a lista completa de dispositivos no Test Lab.

O CameraX usa um Executor para orientar a pilha da câmera. Você pode definir seu próprio executor no CameraX caso seu app tenha requisitos de linha de execução específicos. Se ele não for definido, o CameraX vai criar e usar um Executor interno padrão otimizado. Muitas das APIs de plataforma em que o CameraX foi criado exigem o bloqueio da comunicação entre processos (IPC) com hardwares que, às vezes, podem levar centenas de milissegundos para responder. Por isso, o CameraX só chama essas APIs por linhas de execução em segundo plano, o que garante que a linha de execução principal não seja bloqueada e que a interface continue fluida. Leia mais sobre as linhas de execução.

Se o mercado-alvo do app incluir dispositivos simples, o CameraX vai fornecer uma maneira de reduzir o tempo de configuração com um limitador da câmera. Como o processo de conexão de componentes de hardware pode demorar um pouco, principalmente em dispositivos mais simples, é possível especificar o conjunto de câmeras de que o app precisa. O CameraX só se conecta a essas câmeras durante a configuração. Por exemplo, se o aplicativo usar apenas câmeras traseiras, ele pode definir essa configuração com DEFAULT_BACK_CAMERA e, em seguida, o CameraX evita a inicialização das câmeras frontais para reduzir a latência.

Conceitos de desenvolvimento para Android

Este guia pressupõe uma familiaridade geral com o desenvolvimento para Android. Além de informações básicas, confira alguns conceitos úteis para entender o código abaixo:

Migrar cenários comuns

Esta seção explica como migrar cenários comuns do Camera1 para o CameraX. Cada cenário abrange uma implementação do Camera1, uma implementação do CameraProvider e uma do CameraController do CameraX.

Como selecionar uma câmera

No aplicativo de câmera, uma das primeiras coisas que você pode oferecer é uma maneira de selecionar câmeras diferentes.

Camera1

No Camera1, você pode chamar Camera.open() sem parâmetros para abrir a primeira câmera traseira ou transmitir um ID de número inteiro para a câmera que você quer abrir. Confira um exemplo disso:

// Camera1: select a camera from id.

// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        camera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(TAG, "failed to open camera", e)
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    camera?.release()
    camera = null
}

CameraX: CameraController

No CameraX, a seleção da câmera é processada pela classe CameraSelector. O CameraX facilita o caso comum de usar a câmera padrão. Você pode especificar se quer a câmera frontal padrão ou a câmera traseira padrão. Além disso, o objeto CameraControl do CameraX permite definir o nível de zoom para seu app. Portanto, se ele estiver em execução em um dispositivo com suporte a câmeras lógicas, ele vai mudar para a lente correta.

Confira o código do CameraX para usar a câmera traseira padrão com um CameraController:

// CameraX: select a camera with CameraController

var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector

CameraX: CameraProvider

Confira um exemplo de como selecionar a câmera frontal padrão com CameraProvider. A câmera frontal ou traseira pode ser usada com CameraController ou CameraProvider:

// CameraX: select a camera with CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Set up UseCases (more on UseCases in later scenarios)
    var useCases:Array = ...

    // Set the cameraSelector to use the default front-facing (selfie)
    // camera.
    val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

Também é possível controlar qual câmera vai ser selecionada no CameraX ao usar CameraProvider chamando getAvailableCameraInfos(), o que proporciona um objeto CameraInfo para verificar determinadas propriedades da câmera, como isFocusMeteringSupported(). Depois, você pode convertê-la em um CameraSelector para ser usado como nos exemplos acima com o método CameraInfo.getCameraSelector().

Para conseguir mais detalhes sobre cada câmera, use a classe Camera2CameraInfo. Chame getCameraCharacteristic() com uma chave para os dados da câmera que você quiser. Confira na classe CameraCharacteristics uma lista de todas as chaves que podem ser consultadas.

Este é um exemplo com uma função checkFocalLength() personalizada que você pode definir:

// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().

val cameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

Como mostrar uma visualização

A maioria dos aplicativos de câmera precisa mostrar a transmissão da câmera em algum momento. Com o Camera1, você precisa gerenciar corretamente os callbacks do ciclo de vida e determinar a rotação e o dimensionamento da visualização.

Além disso, no Camera1, você precisa decidir se quer usar uma TextureView ou uma SurfaceView como plataforma de visualização. As duas opções têm compensações e, nos dois casos, o Camera1 exige que você processe a rotação e o dimensionamento corretamente. A PreviewView do CameraX, por outro lado, tem implementações subjacentes para TextureView e SurfaceView. O CameraX decide qual implementação é melhor, dependendo de fatores como o tipo de dispositivo e a versão do Android em que o app está sendo executado. Se uma das implementações for compatível, vai ser possível declarar sua preferência com PreviewView.ImplementationMode. A opção COMPATIBLE usa uma TextureView para a visualização, e o valor PERFORMANCE usa uma SurfaceView (quando possível).

Camera1

Para mostrar uma visualização, é necessário programar sua própria classe Preview com uma implementação da interface android.view.SurfaceHolder.Callback, que é usada para transmitir dados de imagem do hardware da câmera para o aplicativo. Em seguida, antes de iniciar a visualização da imagem em tempo real, a classe Preview precisa ser transmitida para o objeto Camera.

// Camera1: set up a camera preview.

class Preview(
        context: Context,
        private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {

    private val holder: SurfaceHolder = holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // The Surface has been created, now tell the camera
        // where to draw the preview.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "error setting camera preview", e)
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Take care of releasing the Camera preview in your activity.
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                w: Int, h: Int) {
        // If your preview can change or rotate, take care of those
        // events here. Make sure to stop the preview before resizing
        // or reformatting it.
        if (holder.surface == null) {
            return  // The preview surface does not exist.
        }

        // Stop preview before making changes.
        try {
            camera.stopPreview()
        } catch (e: Exception) {
            // Tried to stop a non-existent preview; nothing to do.
        }

        // Set preview size and make any resize, rotate or
        // reformatting changes here.

        // Start preview with new settings.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: Exception) {
                Log.d(TAG, "error starting camera preview", e)
            }
        }
    }
}

class CameraActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private var camera: Camera? = null
    private var preview: Preview? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create an instance of Camera.
        camera = getCameraInstance()

        preview = camera?.let {
            // Create the Preview view.
            Preview(this, it)
        }

        // Set the Preview view as the content of the activity.
        val cameraPreview: FrameLayout = viewBinding.cameraPreview
        cameraPreview.addView(preview)
    }
}

CameraX: CameraController

No CameraX, você, o desenvolvedor, tem muito menos a gerenciar. Se você usar um CameraController, também vai ser necessário usar PreviewView. Isso significa que o UseCase Preview está implícito, tornando a configuração muito menos eficiente:

// CameraX: set up a camera preview with a CameraController.

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create the CameraController and set it on the previewView.
        var cameraController = LifecycleCameraController(baseContext)
        cameraController.bindToLifecycle(this)
        val previewView: PreviewView = viewBinding.cameraPreview
        previewView.controller = cameraController
    }
}

CameraX: CameraProvider

Com o CameraProvider do CameraX, você não precisa usar PreviewView, mas ele ainda simplifica bastante a configuração de visualização em relação ao Camera1. Para fins de demonstração, este exemplo usa uma PreviewView, mas é possível programar um SurfaceProvider personalizado para transmitir para setSurfaceProvider() caso você tenha necessidades mais complexas.

Aqui, o UseCase da Preview não está implícito como no CameraController, então você precisa configurá-lo:

// CameraX: set up a camera preview with a CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Create Preview UseCase.
    val preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(
                viewBinding.viewFinder.surfaceProvider
            )
        }

    // Select default back camera.
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

Toque para focar

Quando a visualização da câmera aparece na tela, um controle comum é definir o ponto de foco quando o usuário toca na visualização.

Camera1

Para implementar o "toque para focar" no Câmera1, calcule a Area de foco ideal para indicar onde a Camera precisa tentar se concentrar. Essa Area é transmitida para setFocusAreas(). Além disso, é necessário definir um modo de foco compatível na Camera. A área de foco só vai ter efeito se o modo de foco atual for FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO ou FOCUS_MODE_CONTINUOUS_PICTURE.

Cada Area é um retângulo com peso especificado. O peso é um valor entre 1 e 1.000 e é usado para priorizar a Areas de foco se várias estiverem definidas. Este exemplo usa apenas uma Area, então o valor do peso não importa. As coordenadas do retângulo variam de -1.000 a 1.000. O ponto superior esquerdo é (-1000, -1000). O ponto inferior direito é (1000, 1000). A direção é relativa à orientação do sensor, ou seja, ao que o sensor vê. A direção não é afetada pela rotação ou espelhamento da Camera.setDisplayOrientation(). Portanto, é necessário converter as coordenadas do evento de toque nas coordenadas do sensor.

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX: CameraController

CameraController detecta os eventos de toque da PreviewView para processar o "toque para focar" automaticamente. Você pode ativar e desativar o "toque para focar" com setTapToFocusEnabled() e verificar o valor com o getter correspondente isTapToFocusEnabled().

O método getTapToFocusState() retorna um objeto LiveData para rastrear mudanças no estado de foco no CameraController.

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer { state ->
    when (state) {
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

CameraX: CameraProvider

Ao usar um CameraProvider, é necessário configurá-lo para que o "toque para focar" esteja funcionando. Neste exemplo, presumimos que você esteja usando PreviewView. Caso contrário, será necessário adaptar a lógica para aplicar à Surface personalizada.

Siga estas etapas ao usar PreviewView:

  1. Configure um detector de gestos para processar eventos de toque.
  2. Com o evento de toque, crie um MeteringPoint usando MeteringPointFactory.createPoint().
  3. Com o MeteringPoint, crie uma FocusMeteringAction.
  4. Com o objeto CameraControl na Camera (retornado de bindToLifecycle()), chame startFocusAndMetering(), transmitindo a FocusMeteringAction.
  5. (Opcional) Responda ao FocusMeteringResult.
  6. Defina o detector de gestos para responder a eventos de toque em PreviewView.setOnTouchListener().
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    // See pinch-to-zooom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

Fazer gesto de pinça para aplicar zoom

Aumentar e diminuir o zoom de uma visualização é outra manipulação direta comum da visualização da câmera. Com o número cada vez maior de câmeras nos dispositivos, os usuários também esperam que a lente com a melhor distância focal seja selecionada automaticamente como resultado do zoom.

Camera1

Há duas maneiras de aplicar zoom usando o Camera1. O método Camera.startSmoothZoom() é animado do nível de zoom atual para o nível de zoom que você transmite. O método Camera.Parameters.setZoom() vai diretamente para o nível de zoom transmitido. Antes de usar um deles, chame isSmoothZoomSupported() ou isZoomSupported(), respectivamente, para garantir que os métodos de zoom relacionados necessários estejam disponíveis na câmera.

Para implementar o gesto de pinça para aplicar zoom, este exemplo usa setZoom(), já que o listener de toque na plataforma de visualização dispara eventos continuamente à medida que o gesto de pinça acontece, então ele atualiza o nível de zoom imediatamente a cada vez. A classe ZoomTouchListener é definida abaixo e precisa ser definida como um callback para o listener de toque da plataforma de visualização.

// Camera1: implement pinch-to-zoom.

// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val parameters = try {
                camera.parameters
            } catch (e: RuntimeException) {
                return false
            }

            // In case there is any focus happening, stop it.
            camera.cancelAutoFocus()

            // Set the zoom level on the Camera.Parameters, and set
            // the Parameters back onto the Camera.
            val currentZoom = parameters.zoom
            parameters.setZoom(detector.scaleFactor * currentZoom)
        camera.setParameters(parameters)
            return true
        }
    }
)

// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean =
        scaleGestureDetector.onTouchEvent(event)
}

// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
    view.setOnTouchListener(ZoomTouchListener())
}

CameraX: CameraController

De forma parecida com o "toque para focar", o CameraController detecta eventos de toque da PreviewView para processar o gesto de pinça para aplicar zoom automaticamente. É possível ativar e desativar o recurso de pinça para aplicar zoom com setPinchToZoomEnabled() e verificar o valor com o getter correspondente isPinchToZoomEnabled().

O método getZoomState() retorna um objeto LiveData para rastrear mudanças no ZoomState no CameraController.

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX: CameraProvider

Para fazer gesto de pinça para aplicar zoom com CameraProvider, é necessário definir algumas configurações. Se você não estiver usando PreviewView, vai precisar adaptar a lógica a ser aplicada à Surface personalizada.

Siga estas etapas ao usar PreviewView:

  1. Configure um detector de gestos de escalonamento para processar eventos de pinça.
  2. Acesse o ZoomState do objeto Camera.CameraInfo, em que a instância de Camera é retornada quando você chama bindToLifecycle().
  3. Se o ZoomState tiver um valor zoomRatio, salve-o como a proporção de zoom atual. Se não houver zoomRatio em ZoomState, use a taxa de zoom padrão da câmera (1.0).
  4. Pegue o produto da proporção de zoom atual com o scaleFactor para determinar a nova proporção de zoom e transmita-o para CameraControl.setZoomRatio().
  5. Defina o detector de gestos para responder a eventos de toque em PreviewView.setOnTouchListener().
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        // See pinch-to-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

Como tirar uma foto

Esta seção mostra como acionar a captura de fotos se você precisar fazer isso clicando no botão do obturador, depois de um tempo ou em qualquer outro evento de sua escolha.

Camera1

No Camera1, primeiro você define um Camera.PictureCallback para gerenciar os dados da imagem quando ele é solicitado. Confira um exemplo simples de PictureCallback para processar dados de imagem JPEG:

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

Depois, sempre que quiser tirar uma foto, chame o método takePicture() na instância de Camera. Esse método takePicture() tem três parâmetros diferentes para diferentes tipos de dados. O primeiro parâmetro é para um ShutterCallback, que não está definido neste exemplo. O segundo parâmetro é para que um PictureCallback processe os dados brutos (não compactados) da câmera. O terceiro parâmetro é usado neste exemplo, já que é um PictureCallback para processar dados de imagem JPEG.

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX: CameraController

O CameraController do CameraX mantém a simplicidade do Camera1 para captura de imagem implementando um método takePicture() próprio. Aqui, defina uma função para configurar uma entrada MediaStore e tirar uma foto para ser salva nesse local.

// CameraX: define a function that uses CameraController to take a photo.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun takePhoto() {
   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata.
   val outputOptions = ImageCapture.OutputFileOptions
       .Builder(context.getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
       .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken.
   cameraController.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(e: ImageCaptureException) {
               Log.e(TAG, "photo capture failed", e)
           }

           override fun onImageSaved(
               output: ImageCapture.OutputFileResults
           ) {
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}

CameraX: CameraProvider

Tirar uma foto com CameraProvider funciona quase da mesma forma que com CameraController, mas primeiro você precisa criar e vincular um UseCase de ImageCapture para ter um objeto para chamar takePicture() em:

// CameraX: create and bind an ImageCapture UseCase.

// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null

...

// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()

...

// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

Em seguida, sempre que você quiser capturar uma foto, chame ImageCapture.takePicture(). Consulte o código CameraController nesta seção para ver um exemplo completo da função takePhoto().

// CameraX: define a function that uses CameraController to take a photo.

private fun takePhoto() {
    // Get a stable reference of the modifiable ImageCapture UseCase.
    val imageCapture = imageCapture ?: return

    ...

    // Call takePicture on imageCapture instance.
    imageCapture.takePicture(
        ...
    )
}

Gravar um vídeo

A gravação de um vídeo é consideravelmente mais complicada do que os cenários vistos até agora. Cada parte do processo precisa ser configurada corretamente, em geral, com uma ordem específica. Além disso, pode ser necessário verificar se o vídeo e o áudio estão sincronizados ou se há outras inconsistências no dispositivo.

Como você vai ver, o CameraX mais uma vez processa grande parte dessa complexidade.

Camera1

A captura de vídeo com o Camera1 exige um gerenciamento cuidadoso de Camera e MediaRecorder. Os métodos precisam ser chamados em uma ordem específica. Você precisa seguir esta ordem para que o aplicativo funcione corretamente:

  1. Abra a câmera.
  2. Prepare e inicie uma visualização (se o app mostrar o vídeo sendo gravado, o que geralmente é o caso).
  3. Desbloqueie a câmera para usar o MediaRecorder chamando Camera.unlock().
  4. Para configurar a gravação, chame estes métodos em MediaRecorder:
    1. Conecte sua instância de Camera com setCamera(camera).
    2. Chame o método setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. Chame o método setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. Chame setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) para definir a qualidade. Consulte CamcorderProfile para ver todas as opções de qualidade.
    5. Chame o método setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Se o app tiver uma prévia do vídeo, chame setPreviewDisplay(preview?.holder?.surface).
    7. Chame o método setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. Chame o método setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. Chame o método setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. Chame prepare() para finalizar a configuração do MediaRecorder.
  5. Para iniciar a gravação, chame MediaRecorder.start().
  6. Para interromper a gravação, chame estes métodos. Novamente, siga esta ordem exata:
    1. Chame o método MediaRecorder.stop().
    2. Opcionalmente, remova a configuração de MediaRecorder atual chamando MediaRecorder.reset().
    3. Chame o método MediaRecorder.release().
    4. Bloqueie a câmera para que as sessões futuras do MediaRecorder possam usá-la chamando Camera.lock().
  7. Para interromper a visualização, chame Camera.stopPreview().
  8. Por fim, para liberar a Camera para que outros processos possam usá-la, chame Camera.release().

Observe todas essas etapas combinadas:

// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.

// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false

...

private fun prepareMediaRecorder(): Boolean {
    mediaRecorder = MediaRecorder()

    // Unlock and set camera to MediaRecorder.
    camera?.unlock()

    mediaRecorder?.run {
        setCamera(camera)

        // Set the audio and video sources.
        setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        setVideoSource(MediaRecorder.VideoSource.CAMERA)

        // Set a CamcorderProfile (requires API Level 8 or higher).
        setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))

        // Set the output file.
        setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())

        // Set the preview output.
        setPreviewDisplay(preview?.holder?.surface)

        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
        setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)

        // Prepare configured MediaRecorder.
        return try {
            prepare()
            true
        } catch (e: IllegalStateException) {
            Log.d(TAG, "preparing MediaRecorder failed", e)
            releaseMediaRecorder()
            false
        } catch (e: IOException) {
            Log.d(TAG, "setting MediaRecorder file failed", e)
            releaseMediaRecorder()
            false
        }
    }
    return false
}

private fun releaseMediaRecorder() {
    mediaRecorder?.reset()
    mediaRecorder?.release()
    mediaRecorder = null
    camera?.lock()
}

private fun startStopVideo() {
    if (isRecording) {
        // Stop recording and release camera.
        mediaRecorder?.stop()
        releaseMediaRecorder()
        camera?.lock()
        isRecording = false

        // This is a good place to inform user that video recording has stopped.
    } else {
        // Initialize video camera.
        if (prepareVideoRecorder()) {
            // Camera is available and unlocked, MediaRecorder is prepared, now
            // you can start recording.
            mediaRecorder?.start()
            isRecording = true

            // This is a good place to inform the user that recording has
            // started.
        } else {
            // Prepare didn't work, release the camera.
            releaseMediaRecorder()

            // Inform user here.
        }
    }
}

CameraX: CameraController

Com o CameraController do CameraX, você pode alternar os UseCases ImageCapture, VideoCapture e ImageAnalysis de maneira independente, desde que a lista de UseCases possa ser usada simultaneamente. Os UseCases ImageCapture e ImageAnalysis são ativados por padrão, e é por isso que você não precisa chamar setEnabledUseCases() para tirar uma foto.

Para usar um CameraController para gravação de vídeos, primeiro você precisa usar setEnabledUseCases() para permitir o UseCase VideoCapture.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Quando quiser começar a gravar vídeos, chame a função CameraController.startRecording(). Ela pode salvar o vídeo gravado em um File, como no exemplo abaixo. Além disso, é necessário transmitir um Executor e uma classe que implementa OnVideoSavedCallback para processar callbacks de sucesso e erro. Quando a gravação terminar, chame CameraController.stopRecording().

Observação: se você estiver usando o CameraX 1.3.0-alpha02 ou mais recente, há um outro parâmetro AudioConfig que permite ativar ou desativar a gravação de áudio no vídeo. Para ativar a gravação de áudio, confira se você tem permissões de microfone. Além disso, o método stopRecording() foi removido na versão 1.3.0-alpha02, e startRecording() retorna um objeto Recording que pode ser usado para pausar, retomar e interromper a gravação de vídeo.

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX: CameraProvider

Se você estiver usando um CameraProvider, vai ser necessário criar um UseCase VideoCapture e transmitir um objeto Recorder. No Recorder.Builder você pode definir a qualidade do vídeo e, se quiser, uma FallbackStrategy, que processa casos em que um dispositivo não consegue atender às especificações de qualidade desejadas. Em seguida, vincule a instância de VideoCapture ao CameraProvider com os outros UseCases.

// CameraX: create and bind a VideoCapture UseCase with CameraProvider.

// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null

...

// Create a Recorder instance to set on a VideoCapture instance (can be
// added with other UseCase definitions).
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
videoCapture = VideoCapture.withOutput(recorder)

...

// Bind UseCases to camera (adding videoCapture along with preview here, but
// preview is not required to use videoCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, videoCapture)

Nesse ponto, o Recorder pode ser acessado na propriedade videoCapture.output. O Recorder pode iniciar gravações de vídeo que são salvas em um File, ParcelFileDescriptor ou MediaStore. O exemplo usa MediaStore.

No Recorder, há vários métodos para chamar e preparar. Chame prepareRecording() para definir as opções de saída do MediaStore. Se o app tiver permissão para usar o microfone do dispositivo, chame withAudioEnabled() também. Em seguida, chame start() para começar a gravar, transmitindo um contexto e um listener de eventos Consumer<VideoRecordEvent> para processar eventos de gravação de vídeo. Se for bem-sucedido, a Recording retornada vai poder ser usada para pausar, retomar ou interromper a gravação.

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.stop()
       recording = null
       return
   }

   // Create and start a new recording session.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
       .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()

   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .withAudioEnabled()
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(
                           baseContext, msg, Toast.LENGTH_SHORT
                       ).show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "video capture ends with error",
                             recordEvent.error)
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}

Outros recursos

Temos vários apps completos do CameraX no repositório de exemplos de câmera do GitHub (em inglês). Esses exemplos mostram como os cenários neste guia se encaixam em um app Android completo.

Se você precisar de mais suporte para migrar para o CameraX ou tiver dúvidas sobre o pacote de APIs de câmera do Android, entre em contato com nossa equipe no Grupo de discussão do CameraX.