1. Antes de comenzar
¿Qué tienen de especial los dispositivos plegables?
Los dispositivos plegables son una innovación única de esta generación. Brindan una experiencia exclusiva y, con ella, oportunidades únicas que permiten satisfacer los gustos de los usuarios con funciones como IU en el modo de mesa para usarlos con la modalidad de manos libres.
Requisitos previos
- Conocimientos básicos sobre el desarrollo de apps para Android
- Conocimientos básicos sobre el framework de inserción de dependencias Hilt
Qué compilarás
En este codelab, compilarás una app de cámara con diseños optimizados para dispositivos plegables.
Comenzarás con una app de cámara básica que no responde a ninguna posición del dispositivo ni aprovecha la mejor cámara posterior para selfies mejoradas. Actualizarás el código fuente para mover la vista previa a la pantalla más pequeña cuando el dispositivo no esté desplegado y que reaccione cuando el teléfono se coloque en modo de mesa.
Aunque la aplicación de cámara es el caso de uso más conveniente para esta API, las funciones que aprenderás en este codelab se pueden aplicar a cualquier app.
Qué aprenderás
- Cómo usar Jetpack Window Manager para responder a los cambios de posición
- Cómo mover tu app a la pantalla más pequeña de un dispositivo plegable
Qué necesitarás
- Una versión reciente de Android Studio
- Contar con un dispositivo plegable o un emulador de dispositivo plegable
2. Prepárate
Obtén el código de inicio
- Si tienes Git instalado, simplemente puedes ejecutar el comando que se indica abajo. Para comprobarlo, escribe
git --version
en la terminal o línea de comandos y verifica que se ejecute de forma correcta.
git clone https://github.com/android/large-screen-codelabs.git
- Si no tienes Git, puedes hacer clic en el siguiente botón para descargar todo el código de este codelab: (opcional).
Abre el primer módulo
- En Android Studio, abre el primer módulo en
/step1
.
Si se te pide que uses la versión más reciente de Gradle, actualízala.
3. Ejecuta y observa
- Ejecuta el código del módulo
step1
.
Como puedes ver, es una app de cámara sencilla en la que puedes alternar entre la cámara frontal y la posterior, y ajustar la relación de aspecto. Sin embargo, por el momento, el primer botón de la izquierda no hace nada, pero será el punto de entrada para el modo de selfie con la cámara posterior.
- Ahora, intenta colocar el dispositivo en una posición semiabierta en la que la bisagra forme un ángulo de 90 grados.
Como puedes ver, la app no responde a las diferentes posturas del dispositivo, por lo que el diseño no cambia y deja la bisagra en medio del visor.
4. Más información sobre WindowManager de Jetpack
La biblioteca de Jetpack WindowManager permite que los desarrolladores de apps creen experiencias optimizadas para los dispositivos plegables. Contiene la clase FoldingFeature
que describe un pliegue en una pantalla flexible o una bisagra entre dos paneles físicos de la pantalla. Su API brinda acceso a información importante que se relaciona con el dispositivo:
state()
: DevuelveFLAT
si la bisagra está abierta a 180 grados; de lo contrario, muestraHALF_OPENED
.orientation()
: DevuelveFoldingFeature.Orientation.HORIZONTAL
si el ancho deFoldingFeature
es mayor que la altura; de lo contrario, muestraFoldingFeature.Orientation.VERTICAL
.bounds()
: Proporciona los límites deFoldingFeature
en formatoRect
.
La clase FoldingFeature
incluye información adicional, como occlusionType()
o isSeparating()
, pero este codelab no lo aborda en detalle.
A partir de la versión 1.2.0-beta01, la biblioteca usa WindowAreaController
, una API que permite que el modo de pantalla posterior mueva la ventana actual a la pantalla que se alinea con la cámara posterior, lo que es excelente para tomar selfies con esa cámara y para muchos otros casos de uso.
Cómo agregar dependencias
- Para usar WindowManager de Jetpack en tu app, debes agregar las siguientes dependencias al archivo de
build.gradle
del nivel de módulo:
step1/build.gradle
def work_version = '1.2.0'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"
Ahora puedes acceder a las clases FoldingFeature
y WindowAreaController
en tu app, y usarlas para crear la mejor experiencia de cámara plegable.
5. Implementa el modo de selfie con la cámara posterior
Comienza con el modo de pantalla posterior.
La API de WindowAreaController
te permite usar este modo y proporciona la información y el comportamiento en torno al movimiento de ventanas entre pantallas o áreas de visualización en un dispositivo.
Además, te permite consultar la lista de WindowAreaInfo
que están disponibles actualmente para interactuar.
Con WindowAreaInfo
, puedes acceder a WindowAreaSession
, una interfaz para representar una función del área de ventana activa y el estado de disponibilidad de un WindowAreaCapability.
específico.
- Declara estas variables en tu
MainActivity
:
step1/MainActivity.kt
private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var rearDisplaySession: WindowAreaSession? = null
private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
private var rearDisplayStatus: WindowAreaCapability.Status =
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA
- Inicialízalas en el método
onCreate()
:
step1/MainActivity.kt
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
windowAreaController.windowAreaInfos
.map{info->info.firstOrNull{it.type==WindowAreaInfo.Type.TYPE_REAR_FACING}}
.onEach { info -> rearDisplayWindowAreaInfo = info }
.map{it?.getCapability(rearDisplayOperation)?.status?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
.distinctUntilChanged()
.collect {
rearDisplayStatus = it
updateUI()
}
}
}
- Ahora implementa la función
updateUI()
para habilitar o inhabilitar el botón de selfie posterior, según el estado actual:
step1/MainActivity.kt
private fun updateUI() {
if(rearDisplaySession != null) {
binding.rearDisplay.isEnabled = true
// A session is already active, clicking on the button will disable it
} else {
when(rearDisplayStatus) {
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not supported on this device"
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not currently available
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
binding.rearDisplay.isEnabled = true
// You can enable RearDisplay Mode
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
binding.rearDisplay.isEnabled = true
// You can disable RearDisplay Mode
}
else -> {
binding.rearDisplay.isEnabled = false
// RearDisplay status is unknown
}
}
}
}
Este último paso es opcional, pero es muy útil para aprender todos los estados posibles de una WindowAreaCapability.
.
- Ahora implementa la función
toggleRearDisplayMode
, que cerrará la sesión si la función ya está activa, o llama a la funcióntransferActivityToWindowArea
:
step1/CameraViewModel.kt
private fun toggleRearDisplayMode() {
if(rearDisplayStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
if(rearDisplaySession == null) {
rearDisplaySession = rearDisplayWindowAreaInfo?.getActiveSession(rearDisplayOperation)
}
rearDisplaySession?.close()
} else {
rearDisplayWindowAreaInfo?.token?.let { token ->
windowAreaController.transferActivityToWindowArea(
token = token,
activity = this,
executor = displayExecutor,
windowAreaSessionCallback = this
)
}
}
}
Observa el uso de MainActivity
como WindowAreaSessionCallback
.
La API de Rear Display funciona con un enfoque de objeto de escucha: cuando solicitas mover el contenido a la otra pantalla, inicias una sesión que se muestra a través del método onSessionStarted()
del objeto de escucha. En cambio, si quieres volver a la pantalla interior (y más grande), cierra la sesión para recibir una confirmación en el método de onSessionEnded()
. Para crear un objeto de escucha de este tipo, debes implementar la interfaz WindowAreaSessionCallback
.
- Modifica la declaración
MainActivity
para implementar la interfaz deWindowAreaSessionCallback
:
step1/MainActivity.kt
class MainActivity : AppCompatActivity(), WindowAreaSessionCallback
Ahora, implementa los métodos onSessionStarted
y onSessionEnded
dentro de MainActivity
. Esos métodos de devolución de llamada son extremadamente útiles para recibir notificaciones sobre el estado de la sesión y actualizar la app según corresponda.
Pero esta vez, para mayor simplicidad, solo verifica en el cuerpo de la función si hay algún error y registra el estado.
step1/MainActivity.kt
override fun onSessionEnded(t: Throwable?) {
if(t != null) {
Log.d("Something was broken: ${t.message}")
}else{
Log.d("rear session ended")
}
}
override fun onSessionStarted(session: WindowAreaSession) {
Log.d("rear session started [session=$session]")
}
- Compila y ejecuta la app. Si luego despliegas el dispositivo y presionas el botón de la pantalla posterior, aparecerá un mensaje como el siguiente:
- Selecciona Switch screens now para ver el contenido en la pantalla externa.
6. Implementa el modo de mesa
Ahora es el momento de hacer que tu app funcione en dispositivos plegables. Para ello, mueve el contenido a un lado o por encima de la bisagra del dispositivo según la orientación del pliegue. En ese caso, actuarás dentro de FoldingStateActor
, de modo que tu código quede desvinculado de Activity
para facilitar la legibilidad.
La parte central de esta API consiste en la interfaz de WindowInfoTracker
, que se crea con un método estático que requiere una Activity
:
step1/CameraCodelabDependencies.kt
@Provides
fun provideWindowInfoTracker(activity: Activity) =
WindowInfoTracker.getOrCreate(activity)
No necesitas escribir este código porque ya existe, pero es útil comprender cómo se crea WindowInfoTracker
.
- Para detectar cualquier cambio en la ventana, hazlo en el método
onResume()
de tuActivity
:
step1/MainActivity.kt
lifecycleScope.launch {
foldingStateActor.checkFoldingState(
this@MainActivity,
binding.viewFinder
)
}
- Ahora, abre el archivo
FoldingStateActor
para completar el métodocheckFoldingState()
.
Este se ejecuta en la fase RESUMED
de tu Activity
y aprovecha la WindowInfoTracker
para detectar cualquier cambio en el diseño.
step1/FoldingStateActor.kt
windowInfoTracker.windowLayoutInfo(activity)
.collect { newLayoutInfo ->
activeWindowLayoutInfo = newLayoutInfo
updateLayoutByFoldingState(cameraViewfinder)
}
Si usas la interfaz de WindowInfoTracker
, puedes llamar a windowLayoutInfo()
para recopilar un Flow
de WindowLayoutInfo
que contenga toda la información disponible en DisplayFeature
.
El último paso es reaccionar a estos cambios y mover el contenido según corresponda. Esto lo haces en el método updateLayoutByFoldingState()
, un paso a la vez.
- Asegúrate de que
activityLayoutInfo
contenga algunas propiedades deDisplayFeature
y que al menos una de ellas sea unaFoldingFeature
. De lo contrario, no realices ninguna acción:
step1/FoldingStateActor.kt
val foldingFeature = activeWindowLayoutInfo?.displayFeatures
?.firstOrNull { it is FoldingFeature } as FoldingFeature?
?: return
- Calcula la posición del pliegue para asegurarte de que la posición del dispositivo influye en el diseño y no está fuera de los límites de la jerarquía:
step1/FoldingStateActor.kt
val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
foldingFeature,
cameraViewfinder.parent as View
) ?: return
Ahora te aseguraste de que cuentas con un FoldingFeature
que influye en tu diseño, por lo que tienes que mover el contenido.
- Verifica que
FoldingFeature
esté enHALF_OPEN
o, de lo contrario, solo restablecerás la posición del contenido. Si aparece comoHALF_OPEN
, tendrás que ejecutar otra verificación y realizar otras acciones según la orientación del pliegue:
step1/FoldingStateActor.kt
if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
when (foldingFeature.orientation) {
FoldingFeature.Orientation.VERTICAL -> {
cameraViewfinder.moveToRightOf(foldPosition)
}
FoldingFeature.Orientation.HORIZONTAL -> {
cameraViewfinder.moveToTopOf(foldPosition)
}
}
} else {
cameraViewfinder.restore()
}
Si el pliegue es VERTICAL
, mueve tu contenido a la derecha. De lo contrario, muévelo a la parte superior de la posición del pliegue.
- Compila y ejecuta tu app y, luego, despliega el teléfono y colócalo en el modo de mesa para ver cómo el contenido se mueve según el caso.
7. ¡Felicitaciones!
En este codelab, aprendiste sobre algunas capacidades que son exclusivas de los dispositivos plegables, como el modo de pantalla posterior o el modo de mesa, y cómo desbloquearlas con Jetpack WindowManager.
Estás listo para implementar experiencias del usuario excelentes en tu app de cámara.