Ilustración de Virginia Poltrack

Animando en un horario

Animaciones en la aplicación Google I / O

Recientemente formé parte de un gran equipo trabajando en la aplicación de Android Google I / O 2018. Esta es una aplicación complementaria de la conferencia, que permite a los asistentes y personas remotas encontrar sesiones, crear un horario personalizado y reservar asientos en el lugar (¡si tienes la suerte de estar allí!). Creamos una serie de características animadas interesantes en la aplicación que creo que mejoraron enormemente la experiencia. El código para esta aplicación acaba de ser de código abierto y quería destacar algunas de estas instancias y algunos detalles de implementación interesantes.

Algunos elementos animados en la aplicación de E / S

En general, hay 3 tipos de animaciones que utilizamos en la aplicación:

  1. Animaciones de héroes: se utilizan para reforzar la marca y brindar momentos de deleite
  2. Transiciones de pantalla
  3. Cambios de estado

Me gustaría entrar en detalles de algunos de estos.

cuenta regresiva

Parte del papel de la aplicación es generar entusiasmo y anticipación para la conferencia. Como tal, este año incluimos una gran cuenta regresiva animada para el inicio de la conferencia que se muestra tanto en la pantalla de incorporación como en la sección Información. Esta también fue una gran oportunidad para integrar la marca del evento en la aplicación, aportando mucho carácter.

La cuenta atrás para el inicio de la conferencia.

Esta animación fue diseñada por un diseñador de movimiento y se entregó como una serie de archivos Lottie json: cada 1 segundo de duración muestra un número que anima "adentro" y luego "afuera". El formato Lottie facilitó la colocación de los archivos en activos e incluso ofreció métodos convenientes como setMinAndMaxProgress que nos permitieron reproducir solo la primera o la última mitad de una animación (para mostrar un número que anima dentro o fuera).

La parte interesante fue en realidad orquestar estas animaciones múltiples en la cuenta regresiva general. Para hacer esto, creamos un CountdownView personalizado que es un RestraintLayout bastante complejo que contiene una serie de LottieAnimationViews. En esto, creamos un delegado de Kotlin para encapsular el inicio de la animación apropiada. Esto nos permitió simplemente asignar un Int a cada delegado del dígito que debería mostrar y el delegado configuraría y comenzaría las animaciones. Extendimos el delegado ObservableProperty, lo que garantiza que solo ejecutamos una animación cuando cambia el dígito. Nuestro bucle de animación simplemente publicó un ejecutable cada segundo (cuando se adjunta la vista) que calcula qué dígito debe mostrar cada vista y actualiza los delegados.

Reserva

Una de las acciones clave de la aplicación es permitir que los asistentes reserven asientos. Como tal, mostramos esta acción prominentemente en un FAB en la pantalla de detalles de la sesión. Sentimos que era importante informar solo que la sesión estaba reservada una vez que se había completado con éxito en el backend (a diferencia de acciones menos importantes como protagonizar una sesión en la que optimizamos de manera optimista la IU de inmediato). Esto puede llevar un poco de tiempo mientras esperamos una respuesta del back-end, por lo que para que sea más receptivo, utilizamos un ícono animado para proporcionar comentarios de que estamos trabajando en él y hacer una transición sin problemas al nuevo estado.

Comentarios al reservar un asiento en una sesión

Esto se complica por el hecho de que hubo varios estados que este ícono necesitaba reflejar: la sesión podría ser reservable, es posible que ya hayan reservado un asiento, si la sesión está llena, entonces puede haber una lista de espera disponible o pueden estar en el lista de espera o cerca de la sesión que inicia las reservas están deshabilitadas. Esto dio lugar a muchas permutaciones de diferentes estados para animar. Para simplificar estas transiciones, decidimos pasar siempre por un estado "en funcionamiento"; el reloj de arena animado de arriba. Por lo tanto, cada transición es en realidad un par de: estado 1 → trabajando y trabajando → estado 2. Esto simplificó mucho las cosas. Construimos cada una de estas animaciones usando cambiaformas; vea los archivos avd_state_to_state aquí.

Para mostrar esto, utilizamos una vista personalizada y un AnimatedStateListDrawable (ASLD). Si no ha usado ASLD antes, es (como su nombre lo indica) una versión animada de StateListDrawable que probablemente haya encontrado, lo que le permite no solo proporcionar diferentes dibujos por estado sino también transiciones entre estados (en forma de AnimatedVectorDrawable o un AnimationDrawable). Aquí está el dibujo que define las imágenes estáticas y las transiciones dentro y fuera del estado de trabajo para el icono de reserva.

Creamos una vista personalizada para admitir nuestros propios estados personalizados. Las vistas ofrecen algunos estados estándar como presionado o seleccionado. Del mismo modo, puede definir la suya propia y tener la ruta Ver que muestra cualquier Drawables que esté mostrando. Definimos nuestro propio estado_reservable, estado_reservado, etc. Luego creamos una enumeración de estos diferentes estados, encapsulando el estado de la vista más cualquier atributo relacionado, como una descripción de contenido asociada. Nuestra lógica de negocios podría simplemente establecer el valor apropiado de esta enumeración en la vista (a través del enlace de datos) que actualizaría el estado del dibujo, que inició una animación a través del ASLD. La combinación de estados personalizados y AnimatedStateListDrawable fue una forma ordenada de implementar esto, manteniendo la multitud de estados en las capas declarativas, lo que resultó en un código de vista mínimo.

Transición del orador

Muchas de las transiciones de pantalla funcionaron bien con las animaciones de ventanas estándar. Un lugar en el que nos desviamos de esto es la transición a la pantalla de detalles del orador. Esto mostraba la imagen de los altavoces a ambos lados de la transición y era un candidato perfecto para una transición de elementos compartidos. Esto ayuda a facilitar el cambio de contexto entre pantallas.

Una transición de elemento compartido

Esta es una transición de elemento compartido bastante estándar, que utiliza las clases ChangeBounds y ArcMotion de la plataforma en un ImageView.

Lo más interesante es cómo la iniciación de esta transición se ajustó al patrón de eventos que utilizamos para la navegación. Esencialmente, este patrón desacopla los eventos de entrada (como grabar en un altavoz) de los eventos de navegación, poniendo al ViewModel a cargo de cómo responder a la entrada. En este caso, este desacoplamiento significa que ViewModel expuso un LiveData of Events, que solo conocía la ID del hablante para navegar. Iniciar una transición de elemento compartido requiere la Vista compartida, que no teníamos en este momento. Resolvimos esto almacenando la ID del hablante como una etiqueta en la vista cuando está vinculada, de modo que la vista se pueda recuperar más tarde cuando necesitemos navegar a una pantalla de detalles del orador en particular.

Filtros

Una parte central de la aplicación de la conferencia es filtrar los muchos eventos a aquellos que le interesan. Cada tema tenía un color asociado para un fácil reconocimiento y recibimos un gran diseño para usar un "chip" personalizado al seleccionar los filtros:

Chips de filtro animados

Observamos Chip de Componentes de material pero optamos por implementar nuestra propia vista personalizada para un mayor control sobre la visualización y la animación entre los estados "marcados". Esto se implementa mediante el dibujo del lienzo y un StaticLayout para mostrar texto. La vista tiene una única propiedad de progreso [0–1] modelado desmarcado – verificado. Para alternar el estado, simplemente animamos este valor e invalidamos la vista y el código de representación interpola linealmente posiciones y tamaños de elementos basados ​​en esto.

Inicialmente, al implementar esto, hice que la vista implementara la interfaz Checkable y comencé la animación cuando el método setChecked estableció un nuevo estado. A medida que mostramos varios filtros en un RecyclerView, esto tuvo el desafortunado efecto de ejecutar la animación si un filtro seleccionado se desplazó hacia afuera y la vista se recuperó a un filtro no seleccionado desplazándose hacia adentro. Whoops. Por lo tanto, agregamos un método separado para iniciar la animación que nos permite diferenciar entre un clic y una actualización inmediata al vincular nuevos datos a la vista.

Además, cuando presentamos esta animación de alternancia, descubrimos que era un error, es decir, soltar cuadros. ¿Fue mi código de animación el culpable? Estos filtros se muestran en una Hoja de fondo frente a la pantalla principal de programación de la conferencia. Cuando se alterna un filtro, iniciamos la lógica de filtrado que se aplicará a la programación (y actualizamos el número de eventos coincidentes en el título de la hoja de filtro). Más adelante, cuando se aplicaron los filtros, el ViewPager de RecyclerViews que muestra la programación se apagó y actualizó a los datos recién entregados. Esto provocó que varias vistas se inflaran y enlazaran. Todo este trabajo estaba arruinando nuestro presupuesto marco ... pero el calendario de actualización no era visible ya que estaba detrás de la hoja de filtro. Tomamos la decisión de retrasar la realización del filtrado real hasta que se ejecutara la animación, intercambiamos un poco más de complejidad de implementación para una experiencia de usuario más fluida. Inicialmente implementé esto usando postDelayed pero esto causó problemas para las pruebas de IU. En cambio, cambiamos nuestro método que comenzó la animación para aceptar una lambda que se ejecute de forma continua. Esto nos permitió respetar mejor la configuración de animación del usuario y probar la ejecución correctamente.

Animate

En general, siento que las animaciones realmente contribuyeron a la experiencia, el carácter, la marca y la capacidad de respuesta de la aplicación. Esperemos que esta publicación haya ayudado a explicar por qué y cómo se usaron y le haya dado sugerencias para su implementación.