Tutorial de Jetpack Compose: Replicar la aplicación Dribble Audio – Transiciones de pantalla | por exyte | marzo 2023
Implementación de una interfaz de usuario totalmente personalizada con animaciones complejas: cambio entre pantallas
En Exyte tratamos de contribuir tanto como sea posible al código abierto, desde la publicación de bibliotecas y componentes hasta la redacción de artículos y tutoriales. Un tipo de tutoriales que hacemos es la replicación: tomamos un componente de interfaz de usuario complejo, lo implementamos con nuevos marcos y escribimos un tutorial junto con él. Comenzamos con SwiftUI hace algunos años, pero esta vez finalmente nos dirigimos a Android utilizando el marco de trabajo de interfaz de usuario declarativo de Google: Jetpack Compose. Este es el tercer artículo de la serie Dribbble Replicating. La publicación anterior demostró la implementación del encabezado colapsado. En esta parte, discutiremos las transiciones entre las vistas de la aplicación.En el antiguo AndroidView, podíamos usar la transición de elementos compartidos como parte del marco de transición. Pero en Compose no hay una solución estándar. En general, no es tan difícil escribirlos usted mismo; describamos lo que debemos hacer con los elementos compartidos:
- Animar la posición de la pantalla.
- tamaño animado.
- Animare el tamaño de la esquina.
Entonces, necesitamos conocer todos estos parámetros en los estados inicial y objetivo. Comencemos escribiendo una clase de datos: Clase de datos SharedElementParams(val initialOffset: Offset,val targetOffset: Offset,val initialSize: Dp,val targetSize: Dp,val initialCornerRadius: Dp,val targetCornerRadius: Dp,)Radio de esquina inicial que podemos establecer .clip(RoundedCornerShape(initialCornerRadius))Initial Offset y initialSize que podemos obtener usando .onGloballyPositioned. Combinando todo lo que obtenemos: var parentOffset of Remember { mutableStateOf(Offset.Unspecified) }var mySize of Remember { mutableStateOf(0) }Image (modifier = Modifier.aspectRatio(1f).onGloballyPositioned {coordenadas ->parentOffset = coordenadas .positionInRoot() mySize = coordenadas.size.width}.clip(RoundedCornerShape(initialCornerRadius)).clickable { onClick(info, parentOffset, mySize) } ,)Ahora necesitamos averiguar los parámetros de destino para la posición final.
- Desplazamiento de destino. Para encontrar el desplazamiento x de un elemento al centro, debemos restar el ancho del elemento del ancho de la pantalla y dividirlo por dos. Establecemos el desplazamiento de altura nosotros mismos:
targetOffset = Offset(x = (maxContentWidth – sharedElementTargetSize.toPx(LocalDensity.current)) / 2f,y = 50.dp.toPx(LocalDensity.current).toFloat()),2. También hemos establecido targetSize.3. Establecemos targetCornerRadius a la mitad del tamaño del elemento compartido. Y dado que nuestro elemento es redondo: targetCornerRadius = sharedElementTargetSize / 2La idea básica es que en el momento de la transición en la nueva función, ponemos el elemento compartido en el estado que tenía en la función componible anterior, y luego lo ponemos en el estado animado debería estar en . Podemos usar LaunchedEffect para iniciar la animación de progreso. 1f else 0f,animationSpec = tween(ANIM_DURATION),)onTransitionFinished()}} Dado que necesitamos animar varias cosas, solo podemos animar offsetProgress (la animación que mostramos arriba) y usar interpolación lineal para capturar cambios en otros parámetros val cornersSize = lerp(params.initialCornerRadius,params.targetCornerRadius,offsetProgress.value,)val currentOffset = lerp(initialOffset,targetOffset,offsetProgress.value)val cornersSize = lerp(params.initialCornerRadius,params.targetCorne rRadius,offsetProgress.value ,)Para que pueda sharedElement:@Composablefun SharedElementContainer(modifier: Modifier = Modifier,coverOffset: Offset,coverSize: Dp,coverCornersRadius: Dp,sharedElement: @Composable BoxScope.() -> Unit,) {//…Box( modificador = modificador.padding (arriba = coverOffset.y.toDp(densidad)).offset {IntOffset(x = coverOffset.x.toInt(), y = 0)}.size(coverSize).clip(RoundedCornerShape(coverCornersRadius)) ,content = sharedElement, )//…}Y eso es todo S ¡Necesitas animar la transición del elemento común! Ahora veamos la transición con el gesto de arrastrar.En nuestro tema, tenemos un botón arrastrable que puede usar para pasar a la siguiente pantalla tocando o deslizando hacia la derecha. Para escribir usamos: .clickable { onClick() } Para gestos de arrastre podemos usar:. pointerInput(Unit) {detectHorizontalDragGestures(onDragStart = {onDragStart()},onDragEnd = onDragFinished,){ change, dragAmount ->onOffsetChange(dragAmount)}}onDragStart se usa para cambiar el estado de la pantalla para comenzar a dibujar la nueva pantalla que aparece. onDragEnd se usa para ver en qué dirección terminará la animación. Cuando el usuario completa el gesto y levanta el dedo hacia el centro de la pantalla, la pantalla vuelve a su posición anterior. Si el gesto del usuario termina más allá de la mitad de la pantalla, detenemos la animación y cambiamos a una nueva pantalla.Usamos este método para determinar qué acción tomar cuando finaliza el gesto. fun completeAnimation(currentDragOffset: Float) {val shouldExpand =currentDragOffset > screenState.maxContentWidth / 4fif (shouldExpand) {expand()} else {collapse()}}El método onOffsetChange se usa para establecer el nuevo desplazamiento y el progreso de la transición para las animaciones. calcular NewOffset(dragAmount: Float) { val newOffset = minOf(screenState.currentDragOffset + dragAmount,(screenState.maxContentWidth – draggableButtonSize.width.toPx(density)). toFloat())if (newOffset >= 0) {screenState.currentDragOffset = newOffset}}Progreso para las otras animaciones que calculamos de 0 a 1.val derivado dePlayerControlsToAlbumsListProgress by StateOf { currentDragOffset / maxContentWidth}Basándonos en este progreso, animamos los elementos necesarios. Un cuadro de información de la canción que se mueve hacia arriba, un cuadro de comentarios que se mueve hacia la izquierda, la carátula de un álbum que se acerca y una lista de álbumes que se muestra. Por ejemplo, calculamos playerControlOffset usando CubicBezierEasing para un cambio de compensación no lineal: valcubicBezierEasing = CubicBezierEasing(a = 0.25f,b = -songInfoContainerHeight.toFloat() / 5,c = 0.5f,d = -10.dp.toPxf( densidad))cubicBezierEasing .transform(fromPlayerControlsToAlbumsListProgress) + songInfoOffset} Esto completa las transiciones entre las vistas de la aplicación. La quinta y última entrega trata de encontrar y solucionar problemas de rendimiento. Próximamente publicación!