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 | VideoCapture | 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 llamadafindViewById()
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:- Agrega
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
a tu archivo Gradle. - Coloca cualquier código de CameraX que muestre un
ListenableFuture
en un bloquelaunch
o una función de suspensión. - Agrega una llamada
await()
a la llamada a función que muestra un objetoListenableFuture
. - Para comprender mejor el funcionamiento de las corrutinas, consulta la guía Cómo iniciar una corrutina.
- Agrega
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
:
- Configura un detector de gestos para controlar los eventos de presión.
- Con el evento de presión, crea un
MeteringPoint
conMeteringPointFactory.createPoint()
. - Con el
MeteringPoint
, crea unaFocusMeteringAction
. - Con el objeto
CameraControl
en tuCamera
(que se muestra desdebindToLifecycle()
), llama astartFocusAndMetering()
y pasa laFocusMeteringAction
. - (Opcional) Responde al
FocusMeteringResult
. - 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
:
- Configura un detector de gestos de escala para controlar los eventos de pellizcar.
- Obtén el
ZoomState
del objetoCamera.CameraInfo
, en el que se muestra la instanciaCamera
cuando llamas abindToLifecycle()
. - Si
ZoomState
tiene un valorzoomRatio
, guárdalo como la relación de zoom actual. Si no hayzoomRatio
enZoomState
, usa la tasa de zoom predeterminada de la cámara (1.0). - Toma el producto de la relación de zoom actual con el
scaleFactor
para determinar la nueva relación de zoom y pásala aCameraControl.setZoomRatio()
. - 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:
- Abre la cámara.
- Prepara e inicia una vista previa (si tu app muestra el video que se está grabando, que suele ser el caso).
- Desbloquea la cámara para que
MediaRecorder
la use. Para ello, llama aCamera.unlock()
. - Para configurar la grabación, llama a estos métodos en
MediaRecorder
:- Conecta tu instancia de
Camera
consetCamera(camera)
. - Llama a
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
. - Llama a
setVideoSource(MediaRecorder.VideoSource.CAMERA)
. - Llama a
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
para definir la calidad. ConsultaCamcorderProfile
para ver todas las opciones de calidad. - Llama a
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
. - Si tu app tiene una vista previa del video, llama a
setPreviewDisplay(preview?.holder?.surface)
. - Llama a
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
. - Llama a
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
. - Llama a
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
. - Llama a
prepare()
para finalizar la configuración de tuMediaRecorder
.
- Conecta tu instancia de
- Para comenzar a grabar, llama a
MediaRecorder.start()
. - Para detener la grabación, llama a estos métodos. Nuevamente, sigue este orden exacto:
- Llama a
MediaRecorder.stop()
. - De manera opcional, llama a
MediaRecorder.reset()
para quitar la configuración actual deMediaRecorder
. - Llama a
MediaRecorder.release()
. - Bloquea la cámara para que las sesiones futuras de
MediaRecorder
puedan usarla mediante una llamada aCamera.lock()
.
- Llama a
- Para detener la vista previa, llama a
Camera.stopPreview()
. - Por último, para liberar la
Camera
de modo que otros procesos puedan usarla, llama aCamera.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: VideoCaptureprivate 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.