Cómo hacer que una vista personalizada sea interactiva

Prueba el estilo de Compose
Jetpack Compose es el kit de herramientas de IU recomendado para Android. Obtén información para trabajar con diseños en Compose.

Diseñar una IU es solo una parte de la creación de una vista personalizada. También debes hacer que tu vista responda a la entrada del usuario de manera similar a la acción real que estás imitando.

Haz que los objetos de tu app actúen como los objetos reales. Por ejemplo, no permitas que las imágenes de la app desaparezcan y vuelvan a aparecer en otro lugar, ya que los objetos del mundo real no hacen eso. En su lugar, mueve tus imágenes de un lugar a otro.

Los usuarios perciben un comportamiento o una sensación incluso sutiles en una interfaz y reaccionan mejor a las sutilezas que imitan el mundo real. Por ejemplo, cuando los usuarios arrojan un objeto de la IU, dales una sensación de inercia al principio que retrasa el movimiento. Al final del movimiento, dales una sensación de impulso que lleva el objeto más allá del lanzamiento.

En esta página, se muestra cómo usar las funciones del framework de Android para agregar estos comportamientos del mundo real a tu vista personalizada.

Puedes encontrar información relacionada adicional en Descripción general de los eventos de entrada y Descripción general de la animación de propiedades.

Cómo controlar los gestos de entrada

Al igual que muchos otros marcos de trabajo de la IU, Android admite un modelo de evento de entrada. Las acciones del usuario se convierten en eventos que activan devoluciones de llamada, y puedes anular las devoluciones de llamada para personalizar la forma en que tu app responde al usuario. El evento de entrada más común en el sistema Android es el táctil, que activa onTouchEvent(android.view.MotionEvent). Anula este método para controlar el evento, de la siguiente manera:

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return super.onTouchEvent(event)
}

Java

@Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

Los eventos táctiles por sí solos no son particularmente útiles. Las IU táctiles modernas definen las interacciones en términos de gestos, como presionar, tirar, empujar, arrastrar y hacer zoom. Para convertir eventos táctiles sin procesar en gestos, Android proporciona GestureDetector.

Para crear un GestureDetector, pasa una instancia de una clase que implemente GestureDetector.OnGestureListener. Si solo quieres procesar algunos gestos, puedes extender GestureDetector.SimpleOnGestureListener en lugar de implementar la interfaz GestureDetector.OnGestureListener. Por ejemplo, este código crea una clase que extiende GestureDetector.SimpleOnGestureListener y anula onDown(MotionEvent).

Kotlin

private val myListener =  object : GestureDetector.SimpleOnGestureListener() {
    override fun onDown(e: MotionEvent): Boolean {
        return true
    }
}

private val detector: GestureDetector = GestureDetector(context, myListener)

Java

class MyListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
detector = new GestureDetector(getContext(), new MyListener());

Ya sea que uses o no GestureDetector.SimpleOnGestureListener, siempre implementa un método onDown() que muestre true. Esto es necesario porque todos los gestos comienzan con un mensaje onDown(). Si muestras false desde onDown(), al igual que GestureDetector.SimpleOnGestureListener, el sistema da por sentado que deseas ignorar el resto del gesto y que no se llama a los otros métodos de GestureDetector.OnGestureListener. Solo muestra false desde onDown() si deseas ignorar un gesto completo.

Después de implementar GestureDetector.OnGestureListener y crear una instancia de GestureDetector, puedes usar tu GestureDetector para interpretar los eventos táctiles que recibes en onTouchEvent().

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return detector.onTouchEvent(event).let { result ->
        if (!result) {
            if (event.action == MotionEvent.ACTION_UP) {
                stopScrolling()
                true
            } else false
        } else true
    }
}

Java

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = detector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

Cuando pasas onTouchEvent() a un evento táctil que no reconoce como parte de un gesto, muestra false. Luego, puedes ejecutar tu propio código personalizado de detección de gestos.

Cómo crear movimientos físicamente creíbles

Los gestos son una forma poderosa de controlar los dispositivos con pantalla táctil, pero pueden ser contradictorios y difíciles de recordar, a menos que produzcan resultados físicamente posibles.

Por ejemplo, supongamos que deseas implementar un gesto de deslizamiento horizontal que establezca el elemento dibujado en la vista que gira alrededor de su eje vertical. Este gesto tiene sentido si la IU responde moviéndose rápidamente en la dirección del lanzamiento y, luego, disminuyendo la velocidad, como si el usuario empujara un volante y lo hiciera girar.

En la documentación sobre cómo animar un gesto de desplazamiento, se brinda una explicación detallada sobre cómo implementar tu propio comportamiento de scoll. Pero simular la sensación de un volante no es trivial. Se requiere mucha física y matemática para que un modelo de volante funcione correctamente. Afortunadamente, Android proporciona clases de ayuda para simular este y otros comportamientos. La clase Scroller es la base para controlar los gestos de deslizamiento tipo volante.

Para iniciar un lanzamiento, llama a fling() con la velocidad inicial y los valores x e y mínimos y máximos del lanzamiento. Para el valor de velocidad, puedes usar el valor calculado por GestureDetector.

Kotlin

fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
    scroller.fling(
            currentX,
            currentY,
            (velocityX / SCALE).toInt(),
            (velocityY / SCALE).toInt(),
            minX,
            minY,
            maxX,
            maxY
    )
    postInvalidate()
    return true
}

Java

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   scroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
   postInvalidate();
    return true;
}

La llamada a fling() configura el modelo físico para el gesto de deslizamiento. Luego, actualiza el Scroller llamando a Scroller.computeScrollOffset() en intervalos regulares. computeScrollOffset() actualiza el estado interno del objeto Scroller leyendo la hora actual y usando el modelo de física para calcular la posición x e y en ese momento. Llama a getCurrX() y getCurrY() para recuperar estos valores.

La mayoría de las vistas pasan las posiciones x e y del objeto Scroller directamente a scrollTo(). Este ejemplo presenta una pequeña diferencia: utiliza la posición x de desplazamiento actual para establecer el ángulo de rotación de la vista.

Kotlin

scroller.apply {
    if (!isFinished) {
        computeScrollOffset()
        setItemRotation(currX)
    }
}

Java

if (!scroller.isFinished()) {
    scroller.computeScrollOffset();
    setItemRotation(scroller.getCurrX());
}

La clase Scroller calcula las posiciones de desplazamiento, pero no aplica automáticamente esas posiciones a tu vista. Aplica coordenadas nuevas con suficiente frecuencia para que la animación de desplazamiento se vea fluida. Existen dos maneras de hacerlo:

  • Llama a postInvalidate() después de llamar a fling() para forzar un nuevo dibujo. Esta técnica requiere que calcules los desplazamientos de desplazamiento en onDraw() y llames a postInvalidate() cada vez que cambie ese desplazamiento.
  • Configura un ValueAnimator para que se anime mientras dure el lanzamiento y agrega un objeto de escucha que procese las actualizaciones de la animación llamando a addUpdateListener(). Esta técnica te permite animar las propiedades de un View.

Haz que tus transiciones sean fluidas

Los usuarios esperan que una IU moderna haga una transición fluida entre los estados: los elementos de la IU se atenúan y desaparezcan en lugar de aparecer y desaparecer, y los movimientos que comienzan y finalizan sin problemas en lugar de iniciarse y detenerse abruptamente. El framework de animación de propiedades de Android facilita las transiciones fluidas.

Para usar el sistema de animación, cada vez que una propiedad cambie lo que afecta la apariencia de tu vista, no la cambies directamente. En su lugar, usa ValueAnimator para realizar el cambio. En el siguiente ejemplo, modificar el componente secundario seleccionado en la vista hace que toda la vista renderizada rote para que el puntero de selección esté centrado. ValueAnimator cambia la rotación durante un período de varios cientos de milisegundos, en lugar de establecer de inmediato el nuevo valor de rotación.

Kotlin

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0).apply {
    setIntValues(targetAngle)
    duration = AUTOCENTER_ANIM_DURATION
    start()
}

Java

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0);
autoCenterAnimator.setIntValues(targetAngle);
autoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
autoCenterAnimator.start();

Si el valor que quieres cambiar es una de las propiedades básicas View, realizar la animación es aún más fácil, ya que las vistas tienen un ViewPropertyAnimator integrado que está optimizado para la animación simultánea de varias propiedades, como en el siguiente ejemplo:

Kotlin

animate()
    .rotation(targetAngle)
    .duration = ANIM_DURATION
    .start()

Java

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();