David Gómez Rubio

Analista funcional

Project Manager

Dirección de equipos informáticos

David Gómez Rubio

Analista funcional

Project Manager

Dirección de equipos informáticos

Artículo del Blog

Crear un Slider estilo Gmail con la API de animaciones de Ionic

Crear un Slider estilo Gmail con la API de animaciones de Ionic

En mis tutoriales recientes, he estado explicando cómo construir patrones UI / UX más complejos en aplicaciones Ionic con la API de Animaciones Ionic y el soporte mejorado de gestos en Ionic 5. A menudo busco gestos o animaciones impresionantes ya sea que estén en aplicaciones móviles «nativas» tradicionales, o simplemente un concepto de diseñadores, y vea si puedo implementar algo igual o similar en Ionic.

Mi objetivo es ayudar a disipar la percepción de que las interacciones y animaciones interesantes son para el ámbito de las aplicaciones móviles nativas que usan controles de interfaz de usuario nativos, a diferencia de las aplicaciones de Ionic que usan una vista web para alimentar la interfaz de usuario (que creo que es realmente una de las mayores ventajas de Ionic). Con un poco de conocimiento acerca de cómo diseñar gestos y animaciones complejas en un entorno web, a menudo podemos crear resultados que no se pueden distinguir de una interfaz de usuario nativa.

Es por eso que decidí abordar la creación de la función Swipe to Archive que usa la aplicación móvil de Gmail en Ionic. La idea básica es que puede deslizar cualquiera de los correos electrónicos en su bandeja de entrada, y si desliza lo suficiente, el correo electrónico se archivará. Al deslizar, se muestra un icono debajo que implica el resultado del gesto. Si no desliza lo suficiente, el correo electrónico volverá a su lugar normal de descanso. 

En el siguiente tutorial resolveremos las siguiente tareas:

  • Crear un componente completamente autónomo para realizar la funcionalidad.
  • Hacer que un elemento siga un gesto en la pantalla.
  • Detectar un umbral de archivo / no archivo del gesto.
  • Encadenamiento de animaciones para que una animación se reproduzca solo después de que otra haya terminado.
  • Crear una animación de eliminación compleja que le da tiempo a un elemento para «animar» antes de ser eliminado físicamente.
  • Usar la cuadrícula CSS para colocar elementos uno encima del otro.

1. La estructura y el diseño del componente básico

Primero deberá crear un nuevo componente para lo que sea que esté utilizando para construir su aplicación, pero si está utilizando StencilJS como lo estoy haciendo en este tutorial, puede ejecutar el siguiente comando para ayudar a crearlo:

npm run generate

Llamé a mi componente app-swipe-delete pero puedes usar lo que quieras. Primero implementemos la estructura básica del componente, y luego agregaremos la funcionalidad de gestos y animación.

Modifique el componente para reflejar lo siguiente:

import { Component, Host, Element, Event, EventEmitter, h } from '@stencil/core';
import { Gesture, GestureConfig, createGesture, createAnimation } from "@ionic/core";

@Component({
  tag: 'app-swipe-delete',
  styleUrl: 'swipe-delete.css'
})
export class SwipeDelete {

  @Element() hostElement: HTMLElement;
  @Event() deleted: EventEmitter;
  private gesture: Gesture;

  async componentDidLoad(){

    const innerItem = this.hostElement.querySelector('ion-item');
    const style = innerItem.style;
    const windowWidth = window.innerWidth;

  }

  disconnectedCallback(){
    if(this.gesture){
      this.gesture.destroy();
      this.gesture = undefined;
    }
  }

  render() {
    return (
      <Host style={{display: `grid`, backgroundColor: `#2ecc71`}}>
        <div style={{gridColumn: `1`, gridRow: `1`, display: `grid`, alignItems: `center`}}>
          <ion-icon name="archive" style={{marginLeft: `20px`, color: `#fff`}}></ion-icon>
        </div>
        <ion-item style={{gridColumn: `1`, gridRow: `1`}}>
          <slot></slot>
        </ion-item>
      </Host>
    );
  }

}

Hemos configurado todas las importaciones que necesitamos para este componente, que incluye lo que se requiere para crear gestos y usar la API de animaciones iónicas. Si está utilizando angular, tenga en cuenta que puede importar el GestureControllerde @ionic/angulary el uso que en lugar de createAnimation.

Usamos @Element() para tomar una referencia al nodo que representa el elemento que estamos creando en el DOM; si no está usando StencilJS, necesitaría tomar una referencia al elemento DOM de alguna otra manera. También usamos @Event() para configurar un evento deleted, que es otro concepto específico de StencilJS, pero que también se puede lograr de otras maneras como marcos como Angular. Básicamente, queremos indicar en algún momento que el elemento ha sido eliminado, y escucharemos ese evento fuera del componente para determinar cuándo eliminar sus datos de la aplicación (por ejemplo, cuándo eliminar el elemento correspondiente de la matriz que contiene todo en nuestra lista).

En el componentDidLoad que se ejecuta automáticamente cuando el componente se ha cargado, configuramos algunas referencias más a los valores que necesitaremos para definir el gesto y las animaciones. Animaremos los elementos al lado derecho de la pantalla, pero una cosa importante a tener en cuenta es que no queremos mover todo el componente. El componente estará compuesto por dos bloques uno encima del otro: el bloque de atrás permanecerá en su lugar (y mostrará un pequeño ícono de «archivo») y el bloque de arriba se deslizará hacia la derecha. Para lograr esto, tomamos una referencia el <ion-item> que estará dentro de este componente para que podamos manipularlo más tarde. También tomamos una referencia al ancho de la pantalla para saber cuanto necesitamos animar hacia la derecha <ion-item> cuando se está borrando.

El disconnectedCallback solo ejecuta un código de limpieza para cuando se destruye el componente. Luego tenemos la plantilla en sí. Dentro de nuestro componente tenemos un <ion-item> y un genérico <div>. Queremos que <div> muestre el color de fondo y el ícono, y queremos que <ion-item> se ponga encima para mostrar el contenido normal. Para poner estos dos elementos uno encima del otro, es un poco complicado. Podríamos lograr esto con un posicionamiento absoluto, pero eso hará que otras cosas, como posicionar el icono de archivo de la manera que queremos, sean incómodas. En cambio, estamos usando un pequeño truco de CSS Grid para colocar los elementos uno encima del otro. Utilizamos display: grid para hacer uso de CSS Grid, y luego le decimos a ambos elementos que ocupen lo mismo grid-column y grid-row. Entonces en nuestro <div> solo podemos usar align-items: center para que nuestro <ion-icon>esté alineado verticalmente.

La única otra cosa que estamos haciendo aquí es usar un <slot> interior de <ion-item>. La idea básica de una ranura es que proyectará el contenido suministrado al componente dentro del componente en sí (si está familiarizado con Angular, <ng-content> es más o menos lo mismo). Lo que esto significa es que si usamos el componente de esta manera:

<app-swipe-delete>
	Hola
</app-swipe-delete>

Ese contenido se proyectaría dentro de nuestro componente de esta manera:

<div style={{gridColumn: `1`, gridRow: `1`, display: `grid`, alignItems: `center`}}>
  <ion-icon name="archive" style={{marginLeft: `20px`, color: `#fff`}}></ion-icon>
</div>
<ion-item style={{gridColumn: `1`, gridRow: `1`}}>
  Hola
</ion-item>

NOTA: Estamos usando un <ion-item> aquí para hacer uso del estilo predeterminado de Ionic, pero podría crearlo fácilmente con otro <div> genérico en  lugar de <ion-item> si no desea que este componente dependa de Ionic.

2. Configurar el gesto.

Ahora trabajemos en el gesto. Lo que queremos hacer es poder deslizar los elementos del elemento de la lista y hacer que el elemento siga nuestro dedo. Si el elemento se desliza / arrastra lo suficiente, realizará una animación de eliminación.

Modifique el enlace del ciclo de vida de la carga para reflejar lo siguiente:

async componentDidLoad(){

    const innerItem = this.hostElement.querySelector('ion-item');
    const style = innerItem.style;
    const windowWidth = window.innerWidth;

    const options: GestureConfig = {
      el: this.hostElement,
      gestureName: 'swipe-delete',
      onStart: () => {
        style.transition = "";
      },
      onMove: (ev) => {
        if(ev.deltaX > 0){
          style.transform = `translate3d(${ev.deltaX}px, 0, 0)`;
        }
      },
      onEnd: (ev) =>{
        style.transition = "0.2s ease-out";

        if(ev.deltaX > 150){
          style.transform = `translate3d(${windowWidth}px, 0, 0)`;          
        } else {
          style.transform = ''
        }
      }
 }

    this.gesture = await createGesture(options);
    this.gesture.enable();
  }

La idea básica es que traduzcamos el elemento en la dirección X para cualquier deltaX. El valor deltaX cuanto se han movido horizontalmente los usuarios con el dedeo / mouse desde el punto de origen. Si usamos transform: translate para mover nuestro elemento la misma cantidad que el deltaX siguiente, seguirá la entrada de los usuarios en la pantalla. También usamos el método onEnd una vez que el gesto se ha completado para determinar si el gesto se deslizó lo suficiente como para activar una eliminación del archivo archivo. Si se deslizó lo suficientemente lejos, animamos automáticamente el elemento completamente fuera de la pantalla al establecer el valor en el eje X al valor windowWidth.

Como mencioné, no queremos mover todo el componente, ya que necesitamos que parte de él permanezca en su lugar para mostrar el color de fondo y el icono «archivo».

3. Implementando la animación

Ya hemos creado nuestro gesto y tenemos el elemento animando fuera de la pantalla si el elemento se deslizó lo suficiente, pero todavía hay un par de cosas más que debemos hacer. Una vez que el elemento se haya animado fuera de la pantalla, no queremos que el bloque verde que se encuentra detrás de ese elemento y que muestre el icono de archivo permanezca allí indefinidamente, también queremos que se anime. Además de eso, una vez que haya terminado de animar, queremos activar elevento delete para que sepamos que ahora podemos eliminar ese elemento de verdad (en lugar de solo ocultarlo a través de animaciones).

Es importante que esperemos hasta que la animación haya terminado antes de activar ese evento de eliminación; de lo contrario, nuestra animación se cortará. La API de animaciones de Ionic proporciona una manera fácil de hacerlo, ya que podemos usar el método onFinish para activar algún código una vez que se haya completado una animación.

Modifique el enlace del ciclo de vida de la carga para reflejar lo siguiente:

async componentDidLoad(){

    const innerItem = this.hostElement.querySelector('ion-item');
    const style = innerItem.style;
    const windowWidth = window.innerWidth;

    const hostDeleteAnimation = createAnimation()
    .addElement(this.hostElement)
    .duration(200)
    .easing('ease-out')
    .fromTo('height', '48px', '0');

    const options: GestureConfig = {
      el: this.hostElement,
      gestureName: 'swipe-delete',
      onStart: () => {
        style.transition = "";
      },
      onMove: (ev) => {
        if(ev.deltaX > 0){
          style.transform = `translate3d(${ev.deltaX}px, 0, 0)`;
        }
      },
      onEnd: (ev) =>{
        style.transition = "0.2s ease-out";

        if(ev.deltaX > 150){
          style.transform = `translate3d(${windowWidth}px, 0, 0)`;

          hostDeleteAnimation.play()

          hostDeleteAnimation.onFinish(() => {
            this.deleted.emit(true);
          })
          
        } else {
          style.transform = ''
        }
      }
    }

    this.gesture = await createGesture(options);
    this.gesture.enable();
  }

Ahora hemos definido una animación hostDelete que animará el height, todo el componente pasará de 48px a 0, lo que significa que se reducirá al igual que la animación de eliminación de Gmail. Es importante tener en cuenta que animar una propiedad como height puede ser costoso para el rendimiento (básicamente, solo debe animar transform y opacity siempre que sea posible, lograr sus animaciones). Dependiendo del contexto, es posible que desee perfilar el rendimiento de esta animación para asegurarse de que se adapte a sus necesidades.

Entonces todo lo que hacemos es activar esta animación con el método play() dentro de nuestro método onEnd, si corresponde. Lo bueno aquí es que también agregamos una devolución de llamada onFinish para esa animación, de modo que cuando termine se active this.delete.emit(true).

4. Usando el componente

Ahora que hemos creado nuestro componente, echemos un vistazo a cómo usarlo y cómo escuchar ese evento deleted para que podamos eliminar los datos de los elementos de la lista en el momento adecuado.

import { Component, State, h } from '@stencil/core';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})
export class AppHome {

  @State() items = [];

  componentWillLoad(){
    this.items = [
      {uid: 1, subject: 'Hola', message: 'Hola'},
      {uid: 2, subject: 'Hola', message: 'Hola'},
      {uid: 3, subject: 'Hola', message: 'Hola'},
      {uid: 4, subject: 'Hola', message: 'Hola'},
      {uid: 5, subject: 'Hola', message: 'Hola'},
      {uid: 6, subject: 'Hola', message: 'Hola'},
      {uid: 7, subject: 'Hola', message: 'Hola'},
      {uid: 8, subject: 'Hola', message: 'Hola'},
      {uid: 9, subject: 'Hola', message: 'Hola'},
      {uid: 10, subject: 'Hola', message: 'Hola'}
    ]
  }

  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content>
        <ion-list>
          {this.items.map((item) => (
            <app-swipe-delete key={item.uid} onDeleted={() => this.handleDelete(item.uid)}>{item.subject}</app-swipe-delete>
          ))}
        </ion-list>
      </ion-content>
    ];
  }

  handleDelete(uid){

    this.items = this.items.filter((item) => {
      return item.uid !== uid;
    });

    this.items = [...this.items];

  }
}

Todo lo que necesitamos hacer ahora para usar este componente es recorrer una matriz de datos y usar nuestro componente <app-swipe-delete> para cada elemento. Podemos suministrarle el contenido que queramos mostrar dentro del elemento, y también podemos escuchar el evento deleted para manejar la eliminación del elemento de la lista cuando sea necesario.

El resultado debería ser este:

Resumen

Lo que hemos creado es una interacción / animación razonablemente avanzada, pero con el uso del sistema de gestos de Ionic y la API de animaciones de Ionic, en realidad es razonablemente fácil de crear con relativamente poco código.

Escribe un comentario