Cómo migrar Camera1 a CameraX

Si tu app usa la clase Camera original ("Camera1"), que dejó de estar disponible desde Android 5.0 (nivel de API 21), te recomendamos actualizar a una API de cámara de Android moderna. Android ofrece CameraX (una API de cámara estándar y sólida de Jetpack) y Camera2 (una API de framework de bajo nivel). En la mayoría de los casos, te recomendamos que migres tu app a CameraX. Estos son los motivos:

  • Facilidad de uso: CameraX controla los detalles de bajo nivel para que no tengas que preocuparte por crear una experiencia con la cámara desde cero y puedas dedicarte más tiempo a que la app se destaque.
  • CameraX controla la fragmentación por ti: CameraX reduce los costos de mantenimiento a largo plazo y el código específico del dispositivo, lo que brinda experiencias de mayor calidad a los usuarios. Para obtener más información al respecto, consulta nuestra entrada de blog sobre las mejoras de compatibilidad del dispositivo con CameraX.
  • Funciones avanzadas: CameraX se diseñó cuidadosamente para que la funcionalidad avanzada sea fácil de incorporar en la app. Por ejemplo, puedes aplicar de manera sencilla las funciones de bokeh, retoque facial y HDR (alto rango dinámico), además del modo de captura nocturna con poca luz a tus fotos gracias a las extensiones de CameraX.
  • Capacidad de actualización: Android lanza nuevas funciones y correcciones de errores a CameraX durante todo el año. Con la migración a CameraX, tu app obtiene la tecnología de cámara de Android más reciente con cada versión de CameraX, no solo con las versiones anuales de Android.

En esta guía, encontrarás situaciones comunes para las aplicaciones de cámara. Cada situación incluye una implementación de Camera1 y una implementación de CameraX para poder compararlas.

Cuando se trata de la migración, a veces necesitas flexibilidad adicional para integrarla a una base de código existente. Todo el código de CameraX de esta guía tiene una implementación de CameraController (ideal si prefieres la manera más sencilla de usar CameraX) y, además, una implementación de CameraProvider (ideal si necesitas más flexibilidad). Para ayudarte a decidir cuál es la implementación más adecuada para ti, estos son los beneficios de cada una:

CameraController

CameraProvider

Requiere poco código de configuración. Permite un mayor control.
Permite que CameraX controle más del proceso de configuración significa que una funcionalidad como presionar para enfocar y pellizcar para acercar funciona automáticamente. Dado que el desarrollador de apps controla la configuración, existen más oportunidades para personalizar la configuración, como habilitar la rotación de la imagen de salida o configurar el formato de la imagen de salida en ImageAnalysis.
La solicitud de PreviewView para la vista previa de la cámara permite que CameraX ofrezca una integración continua de extremo a extremo, como en la integración de nuestro ML Kit, que puede asignar coordenadas de resultados de modelos de ML (como cuadros delimitadores de rostros) directamente en las coordenadas de vista previa La capacidad de usar una "Surface" personalizada para la vista previa de la cámara proporciona más flexibilidad, como usar tu código existente de "Surface", que podría ser una entrada para otras partes de tu app.

Si tienes problemas para realizar la migración, comunícate con nosotros en el grupo de discusión de CameraX.

Antes de migrar

Comparación entre el uso de CameraX y Camera1

Si bien el código puede tener un aspecto diferente, los conceptos subyacentes de Camera1 y CameraX son muy similares. CameraX abstrae la funcionalidad común de la cámara a los casos de uso y, como resultado, CameraX controla automáticamente muchas de las tareas que el desarrollador tenía a cargo en Camera1. Hay cuatro objetos UseCase en CameraX, que puedes usar para una variedad de tareas de la cámara: Preview, ImageCapture, VideoCapture y ImageAnalysis.

Un ejemplo de CameraX que controla los detalles de bajo nivel para los desarrolladores es el ViewPort, que se comparte entre los UseCase activos. Esto garantiza que todos los UseCase vean exactamente los mismos píxeles. En Camera1, debes administrar estos detalles por tu cuenta. Dada la variabilidad de las relaciones de aspecto entre los sensores de cámara y las pantallas de los dispositivos, puede ser difícil garantizar que la vista previa coincida con las fotos y los videos capturados.

Como otro ejemplo, CameraX controla automáticamente las devoluciones de llamada de Lifecycle en la instancia de Lifecycle. Esto significa que CameraX controla la conexión de tu app con la cámara durante todo el ciclo de vida de la actividad de Android, incluidos los siguientes casos: cerrar la cámara cuando la app pasa al segundo plano; quitar la vista previa de la cámara cuando ya no es necesario que la pantalla la muestre y pausar la vista previa de la cámara cuando otra actividad tenga prioridad en primer plano, como una videollamada entrante.

Por último, CameraX controla la rotación y el escalamiento sin que debas escribir ningún código adicional. En el caso de un objeto Activity con una orientación desbloqueada, se realiza la configuración de UseCase cada vez que se rota el dispositivo, ya que el sistema destruye y vuelve a crear el objeto Activity con los cambios de orientación. Como resultado, los UseCases configuran su rotación objetivo para que coincida con la orientación de la pantalla de forma predeterminada cada vez que se rote el dispositivo. Obtén más información sobre las rotaciones en CameraX.

Antes de pasar a los detalles, aquí se muestra un panorama de alto nivel de los UseCase de CameraX y cómo sería su equivalente de Camera1 (los conceptos de CameraX están en azul y los de Camera1 en verde).

CameraX

Configuración de CameraController/CameraProvider
Vista previa ImageCapture Captura de video ImageAnalysis
Administrar la Surface de vista previa y configurarla en la cámara Configurar PictureCallback y llamar a takePicture() en la cámara Administrar la configuración de la cámara y MediaRecorder en un orden específico Código de análisis personalizado creado sobre la Surface de la vista previa
Código específico del dispositivo
Rotación del dispositivo y administración de escalamiento
Administración de la sesión de la cámara (selección de la cámara y administración del ciclo de vida)

Camera1

Compatibilidad y rendimiento en CameraX

CameraX es compatible con dispositivos que ejecutan Android 5.0 (nivel de API 21) y versiones posteriores. Esta cifra representa más del 98% de los dispositivos Android existentes. CameraX se creó para manejar de forma automática las diferencias entre los dispositivos, lo que reduce la necesidad de usar códigos específicos del dispositivo en tu app. Además, probamos más de 150 dispositivos físicos en todas las versiones de Android desde 5.0 en nuestro Test Lab de CameraX. Puedes revisar la lista completa de dispositivos que se encuentran actualmente en Test Lab.

CameraX usa un Executor para controlar la pila de la cámara. Si tu app tiene requisitos específicos de subprocesos, puedes configurar tu propio ejecutor en CameraX. Si no se configura, CameraX crea y usa un Executor interno optimizado predeterminado. Muchas de las APIs de la plataforma en las que se compiló CameraX requieren el bloqueo de la comunicación entre procesos (IPC) con hardware que a veces puede tardar cientos de milisegundos en responder. Por este motivo, CameraX solo llama a estas APIs desde subprocesos en segundo plano, lo cual garantiza que el subproceso principal no esté bloqueado y que la IU permanezca fluida. Obtén más información sobre las conversaciones.

Si el mercado objetivo de tu app incluye dispositivos de baja gama, CameraX proporciona una forma de reducir el tiempo de configuración con un limitador de cámara. Dado que el proceso de conexión a los componentes de hardware puede tardar una cantidad de tiempo considerable, en especial en dispositivos de baja gama, puedes especificar el conjunto de cámaras que necesita tu app. CameraX solo se conecta a estas cámaras durante la configuración. Por ejemplo, si la aplicación solo usa cámaras traseras, puede establecer esta configuración con DEFAULT_BACK_CAMERA y, luego, CameraX evitará inicializar cámaras frontales para reducir la latencia.

Conceptos de desarrollo de Android

En esta guía, se asume que conoces el desarrollo para Android. Además de los aspectos básico, estos son algunos conceptos útiles de comprender antes de pasar al siguiente código:

  • La vinculación de vistas genera una clase de vinculación para tus archivos de diseño XML, lo que te permite hacer referencia a tus vistas en actividades fácilmente, como se hace en varios fragmentos de código a continuación. Existen algunas diferencias entre la vinculación de vistas y findViewById() (la forma anterior de hacer referencia a vistas), pero deberías poder reemplazar las líneas de vinculación de vistas con una llamada findViewById() similar en el código que se muestra más abajo.
  • Las corrutinas asíncronas son un patrón de diseño de simultaneidad agregado en Kotlin 1.3 que se puede usar para controlar métodos de CameraX que muestran un objeto ListenableFuture. Esto es más fácil con la biblioteca Concurrent de Jetpack a partir de la versión 1.1.0. Para agregar una corrutina asíncrona a tu app, haz lo siguiente:
    1. Agrega implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") a tu archivo Gradle.
    2. Coloca cualquier código de CameraX que muestre un ListenableFuture en un bloque launch o una función de suspensión.
    3. Agrega una llamada await() a la llamada a función que muestra un objeto ListenableFuture.
    4. Para comprender mejor el funcionamiento de las corrutinas, consulta la guía Cómo iniciar una corrutina.

Cómo migrar situaciones comunes

En esta sección, se explica cómo migrar situaciones comunes de Camera1 a CameraX. Cada situación abarca una implementación de Camera1, una implementación CameraProvider de CameraX y una implementación CameraController de CameraX.

Cómo seleccionar una cámara

En la aplicación de la cámara, una de las primeras cosas que podrías ofrecer es una forma de seleccionar diferentes cámaras.

Camera1

En Camera1, puedes llamar a Camera.open() sin parámetros para abrir la primera cámara trasera, o bien pasar un ID de número entero para la cámara que quieres abrir. Este es un ejemplo de cómo podría verse:

// 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

En CameraX, la clase CameraSelector se encarga de la selección de la cámara. CameraX facilita el uso de la cámara predeterminada. Puedes especificar si quieres que se use la cámara frontal o la posterior de forma predeterminada. Además, el objeto CameraControl de CameraX te permite configurar fácilmente el nivel de zoom de tu app, de modo que si esta se ejecuta en un dispositivo compatible con cámaras lógicas, se cambiará a la lente adecuada.

Este es el código de CameraX para usar la cámara posterior de forma predeterminada con un 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

Este es un ejemplo de cómo seleccionar la cámara frontal de forma predeterminada con CameraProvider (se puede usar la cámara frontal o posterior con CameraController o 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()
    }
}

Si quieres controlar qué cámara se selecciona, también puedes hacerlo en CameraX cuando usas CameraProvider con llamadas a getAvailableCameraInfos(), lo que te proporciona un objeto CameraInfo para verificar determinadas propiedades de la cámara como isFocusMeteringSupported(). Luego, puedes convertirlo en un CameraSelector para usarlo como en los ejemplos anteriores con el método CameraInfo.getCameraSelector().

Puedes obtener más detalles sobre cada cámara con la clase Camera2CameraInfo. Llama a getCameraCharacteristic() con una clave para los datos de la cámara que desees. Verifica la clase CameraCharacteristics para obtener una lista de todas las claves que puedes consultar.

Este es un ejemplo en el que se usa una función checkFocalLength() personalizada que puedes definir por tu cuenta:

// 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()

Cómo mostrar una vista previa

La mayoría de las aplicaciones de cámara deben mostrar el feed de la cámara en la pantalla en algún momento. Con Camera1, debes administrar correctamente las devoluciones de llamada de ciclo de vida y determinar la rotación y el escalamiento de tu vista previa.

Además, en Camera1, debes decidir si usar una TextureView o una SurfaceView como plataforma de vista previa. Ambas opciones incluyen compensaciones y, en cualquier caso, Camera1 requiere que controles la rotación y el escalamiento de forma correcta. La PreviewView de CameraX, por otro lado, tiene implementaciones subyacentes para TextureView y SurfaceView. CameraX decide qué implementación es mejor según factores como el tipo de dispositivo y la versión de Android en la que se ejecuta tu app. Si alguna de las implementaciones es compatible, puedes declarar tu preferencia con PreviewView.ImplementationMode. La opción COMPATIBLE usa una TextureView para la vista previa y el valor PERFORMANCE usa una SurfaceView (cuando es posible).

Camera1

Para mostrar una vista previa, debes escribir tu propia clase Preview con una implementación de la interfaz android.view.SurfaceHolder.Callback, que se usa para pasar datos de imágenes del hardware de la cámara a la aplicación. Luego, antes de que puedas iniciar la vista previa de la imagen en vivo, se debe pasar la clase Preview al 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

En CameraX, como desarrollador, tienes mucho menos que administrar. Si usas un CameraController, también debes usar PreviewView. Esto significa que la UseCase de Preview está implícita, lo que hace que la configuración requiera mucho menos trabajo:

// 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

Con el CameraProvider de CameraX, no necesitas usar PreviewView, pero simplifica de manera significativa la configuración de la vista previa en Camera1. A modo de demostración, en este ejemplo, se usa una PreviewView, pero puedes escribir una SurfaceProvider personalizada para pasar a setSurfaceProvider() si tienes necesidades más complejas.

Aquí, el UseCase de la Preview no está implícito, como sucede con CameraController, por lo que debes configurarlo:

// 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()
    }
}

Presionar para enfocar

Cuando la vista previa de la cámara está en pantalla, un control común consiste en establecer el punto de enfoque cuando el usuario presiona la vista previa.

Camera1

Si deseas implementar la función de presionar para enfocar en Camera1, debes calcular el Area de enfoque óptimo para indicar dónde Camera debe enfocar. Este Area se pasa a setFocusAreas(). Además, debes configurar un modo de enfoque compatible en la Camera. El área de enfoque solo tiene efecto si el modo de enfoque actual es FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO o FOCUS_MODE_CONTINUOUS_PICTURE.

Cada Area es un rectángulo con un peso especificado. El peso es un valor entre 1 y 1,000, y se usa para priorizar las Areas de enfoque si se establecen varias. En este ejemplo, solo se usa una Area, por lo que el valor del peso no importa. Las coordenadas del rectángulo varían de -1000 a 1000. El punto superior izquierdo es (-1000, -1000). El punto inferior derecho es (1000, 1000). La dirección es relativa a la orientación del sensor, es decir, lo que ve el sensor. La dirección no se ve afectada por la rotación o duplicación de Camera.setDisplayOrientation(), por lo que debes convertir las coordenadas del evento táctil en las del 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 escucha los eventos táctiles de PreviewView para controlar automáticamente la opción de presionar para enfocar. Puedes habilitar o inhabilitar la función de presionar para enfocar con setTapToFocusEnabled() y verificar el valor del método get correspondiente isTapToFocusEnabled().

El método getTapToFocusState() muestra un objeto LiveData para hacer un seguimiento de los cambios en el estado del enfoque en 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

Cuando se usa un objeto CameraProvider, se requiere cierta configuración para que funcione la opción de presionar para enfocar. En este ejemplo, se supone que usas PreviewView. De lo contrario, debes adaptar la lógica para aplicarla a tu Surface personalizado.

Estos son los pasos para usar PreviewView:

  1. Configura un detector de gestos para controlar los eventos de presión.
  2. Con el evento de presión, crea un MeteringPoint con MeteringPointFactory.createPoint().
  3. Con el MeteringPoint, crea una FocusMeteringAction.
  4. Con el objeto CameraControl en tu Camera (que se muestra desde bindToLifecycle()), llama a startFocusAndMetering() y pasa la FocusMeteringAction.
  5. (Opcional) Responde al FocusMeteringResult.
  6. Configura tu detector de gestos para responder a eventos táctiles en 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
}

Pellizcar para acercar

Acercar y alejar la vista previa es otra manera común de manipular directamente la vista previa de la cámara. Debido al aumento de la cantidad de cámaras en los dispositivos, los usuarios también esperan que la lente con la mejor longitud focal se seleccione automáticamente como resultado del zoom.

Camera1

Hay dos maneras de hacer zoom con Camera1. El método Camera.startSmoothZoom() anima desde el nivel de zoom actual hasta el nivel de zoom que pasas. El método Camera.Parameters.setZoom() salta directamente al nivel de zoom que pasas. Antes de usar cualquiera de ellos, llama a isSmoothZoomSupported() o a isZoomSupported(), respectivamente, para asegurarte de que los métodos de zoom relacionados que necesites estén disponibles en la cámara.

Para implementar la función de pellizcar para acercar, en este ejemplo se usa setZoom() porque el objeto de escucha táctil en la superficie de vista previa activa eventos de forma continua a medida que se produce el gesto de pellizcar, por lo que actualiza el nivel de zoom de inmediato cada vez que sucede. La clase ZoomTouchListener se define a continuación y se debe configurar como una devolución de llamada al objeto de escucha táctil de la superficie de vista previa.

// 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 manera similar a la acción de presionar para enfocar, CameraController escucha los eventos táctiles de PreviewView para controlar la función de pellizcar para acercar automáticamente. Puedes habilitar o inhabilitar la función de pellizcar para acercar con setPinchToZoomEnabled() y verificar el valor con el método get correspondiente isPinchToZoomEnabled().

El método getZoomState() muestra un objeto LiveData para hacer un seguimiento de los cambios en ZoomState en el 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 que la función de pellizcar para acercar funcione con CameraProvider, se requiere cierta configuración. Si no usas PreviewView, debes adaptar la lógica para que se aplique a tu Surface personalizada.

Estos son los pasos para usar PreviewView:

  1. Configura un detector de gestos de escala para controlar los eventos de pellizcar.
  2. Obtén el ZoomState del objeto Camera.CameraInfo, en el que se muestra la instancia Camera cuando llamas a bindToLifecycle().
  3. Si ZoomState tiene un valor zoomRatio, guárdalo como la relación de zoom actual. Si no hay zoomRatio en ZoomState, usa la tasa de zoom predeterminada de la cámara (1.0).
  4. Toma el producto de la relación de zoom actual con el scaleFactor para determinar la nueva relación de zoom y pásala a CameraControl.setZoomRatio().
  5. Configura tu detector de gestos para responder a eventos táctiles en 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
}

Cómo tomar una foto

En esta sección, se muestra cómo activar la captura de fotos, ya sea que debas hacerlo presionando el botón del obturador, después de que haya transcurrido el tiempo de un cronómetro o en cualquier otro evento que elijas.

Camera1

En Camera1, primero debes definir un Camera.PictureCallback para administrar los datos de imagen cuando se solicita. A continuación, se muestra un ejemplo simple de PictureCallback para manejar datos de imágenes 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)
    }
}

Luego, cada vez que quieras tomar una foto, debes llamar al método takePicture() en la instancia Camera. Este método takePicture() tiene tres parámetros diferentes para distintos tipos de datos. El primer parámetro es para una ShutterCallback (que no se define en este ejemplo). El segundo parámetro es para que una PictureCallback controle los datos de la cámara sin procesar (sin comprimir). El tercer parámetro es el que se usa en este ejemplo, ya que es una PictureCallback que permite controlar los datos de imágenes JPEG.

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

camera?.takePicture(null, null, picture)

CameraX: CameraController

CameraController de CameraX mantiene la simplicidad de Camera1 para la captura de imágenes con la implementación de un método takePicture() propio. Aquí, define una función para configurar una entrada MediaStore y tomar una foto que se guardará allí.

// 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

Tomar una foto con CameraProvider funciona casi igual que con CameraController, pero primero debes crear y vincular un UseCase de ImageCapture para tener un objeto al que llamar a takePicture() en:

// 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)

Luego, cuando quieras tomar una foto, podrás llamar a ImageCapture.takePicture(). Consulta el código CameraController de esta sección para ver un ejemplo completo de la función 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(
        ...
    )
}

Cómo grabar un video

Grabar un video es mucho más complicado de lo que se ve en estas situaciones. Cada parte del proceso debe configurarse de forma correcta, por lo general, en un orden específico. Además, quizás debas verificar que el video y el audio estén sincronizados o tendrás que enfrentarte a inconsistencias adicionales del dispositivo.

Como verás, CameraX vuelve a controlar gran parte de esta complejidad.

Camera1

La captura de video con Camera1 requiere una administración cuidadosa de Camera y MediaRecorder, y se debe llamar a los métodos en un orden específico. Debes seguir este orden para que tu aplicación funcione correctamente:

  1. Abre la cámara.
  2. Prepara e inicia una vista previa (si tu app muestra el video que se está grabando, que suele ser el caso).
  3. Desbloquea la cámara para que MediaRecorder la use. Para ello, llama a Camera.unlock().
  4. Para configurar la grabación, llama a estos métodos en MediaRecorder:
    1. Conecta tu instancia de Camera con setCamera(camera).
    2. Llama a setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. Llama a setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. Llama a setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) para definir la calidad. Consulta CamcorderProfile para ver todas las opciones de calidad.
    5. Llama a setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Si tu app tiene una vista previa del video, llama a setPreviewDisplay(preview?.holder?.surface).
    7. Llama a setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. Llama a setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. Llama a setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. Llama a prepare() para finalizar la configuración de tu MediaRecorder.
  5. Para comenzar a grabar, llama a MediaRecorder.start().
  6. Para detener la grabación, llama a estos métodos. Nuevamente, sigue este orden exacto:
    1. Llama a MediaRecorder.stop().
    2. De manera opcional, llama a MediaRecorder.reset() para quitar la configuración actual de MediaRecorder.
    3. Llama a MediaRecorder.release().
    4. Bloquea la cámara para que las sesiones futuras de MediaRecorder puedan usarla mediante una llamada a Camera.lock().
  7. Para detener la vista previa, llama a Camera.stopPreview().
  8. Por último, para liberar la Camera de modo que otros procesos puedan usarla, llama a Camera.release().

A continuación, se muestran todos estos pasos combinados:

// 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

Con CameraController de CameraX, puedes activar o desactivar de forma independiente los UseCase de ImageCapture, VideoCapture y ImageAnalysis, siempre que la lista de UseCases se pueda utilizar de manera simultánea. Los UseCase de ImageCapture y ImageAnalysis están habilitados de forma predeterminada, por lo que no necesitas llamar a setEnabledUseCases() para tomar una foto.

Si quieres usar un CameraController para la grabación de video, primero debes usar setEnabledUseCases() para permitir el UseCase de VideoCapture.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Cuando desees comenzar a grabar video, puedes llamar a la función CameraController.startRecording(). Esta función puede guardar el video grabado en un File, como puedes ver en el siguiente ejemplo. Además, debes pasar un Executor y una clase que implemente OnVideoSavedCallback para controlar las devoluciones de llamada de éxito y error. Cuando la grabación deba finalizar, llama a CameraController.stopRecording().

Nota: Si usas CameraX 1.3.0-alpha02 o una versión posterior, hay un parámetro AudioConfig adicional que te permite habilitar o inhabilitar la grabación de audio en el video. Para habilitar la grabación de audio, debes asegurarte de tener los permisos del micrófono. Además, el método stopRecording() se quita en la versión 1.3.0-alpha02 y startRecording() muestra un objeto Recording que se puede usar para pausar, reanudar y detener la grabación de video.

// 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

Si usas un objeto CameraProvider, debes crear un UseCase de VideoCapture y pasar un objeto Recorder. En Recorder.Builder, puedes configurar la calidad de video y, de manera opcional, un FallbackStrategy, que controla los casos en los que un dispositivo no puede cumplir con tus especificaciones de calidad deseadas. Luego, vincula la instancia VideoCapture a CameraProvider con tus otros UseCase.

// 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)

En este punto, se puede acceder a Recorder en la propiedad videoCapture.output. Recorder puede iniciar grabaciones de video que se hayan guardado en File, ParcelFileDescriptor o MediaStore. En este ejemplo, se usa MediaStore.

En Recorder, hay varios métodos a los que puedes llamar para prepararlo. Llama a prepareRecording() para configurar las opciones de salida de MediaStore. Si tu app tiene permiso para usar el micrófono del dispositivo, también debes llamar a withAudioEnabled(). Luego, llama a start() para comenzar a grabar y pasa un contexto y un objeto de escucha de eventos Consumer<VideoRecordEvent> para controlar los eventos de grabación de video. Si se ejecuta correctamente, se puede usar el objeto Recording que se muestra para pausar, reanudar o detener la grabación.

// 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
                   }
               }
           }
       }
}

Recursos adicionales

Tenemos varias apps de CameraX completas en nuestro repositorio de GitHub de muestras de cámara. Estos ejemplos muestran cómo las situaciones en esta guía encajan en una app para Android completa.

Si quieres obtener asistencia adicional para la migración a CameraX o tienes preguntas sobre el conjunto de APIs de cámara de Android, comunícate con nosotros en el grupo de discusión sobre CameraX.