Cómo hacer una vista interactiva

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. Los objetos siempre deben actuar de la misma manera que los elementos reales. Por ejemplo, las imágenes no deben desaparecer de pronto y reaparecer en otro lugar, porque los elementos del mundo real no hacen eso. En cambio, las imágenes deben moverse de un lugar a otro.

Los usuarios también pueden percibir un comportamiento sutil o un pequeño cambio en una interfaz y reaccionan mejor a las sutilezas que imitan el mundo real. Por ejemplo, cuando los usuarios arrojan un objeto en la IU, deben sentir una fricción al principio que retrasa el movimiento, y luego, al final, detectar el impulso que lleva el movimiento más allá del lanzamiento.

En esta lección, se muestra cómo usar las funciones del marco de trabajo de Android para agregar estos comportamientos del mundo real a tu vista personalizada.

Fuera de esta lección, encontrarás más información relacionada en Eventos de entrada y 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 desencadenan devoluciones de llamada, y puedes anular las devoluciones de llamada para personalizar cómo responde tu aplicación 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:

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í mismos no son particularmente útiles. Las IU táctiles modernas definen las interacciones en términos de gestos, como presionar, tirar, empujar, arrojar y hacer zoom. Para convertir eventos táctiles sin procesar en gestos, Android proporciona GestureDetector.

Crea un GestureDetector pasando 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(PieChart.this.getContext(), new MyListener());
    

Ya sea si usas o no GestureDetector.SimpleOnGestureListener, siempre debes implementar un método onDown() que muestre true. Este paso es necesario porque todos los gestos comienzan con un mensaje onDown(). Si muestras false a partir de onDown(), como lo hace GestureDetector.SimpleOnGestureListener, el sistema supone que quieres omitir el resto del gesto y nunca se llama a los otros métodos de GestureDetector.OnGestureListener. Solo debes mostrar false para onDown() si realmente quieres omitir un gesto completo. Una vez que hayas implementado GestureDetector.OnGestureListener y hayas creado una instancia de GestureDetector, podrás 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 este elemento no reconoce como parte de un gesto, se muestra false. Luego, puedes ejecutar tu propio código personalizado de detección de gestos.

Cómo crear movimientos físicamente posibles

Los gestos son una forma eficaz 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. Un buen ejemplo es el gesto de lanzamiento, donde el usuario mueve rápido un dedo por la pantalla y luego lo levanta. Este gesto tiene sentido si la IU responde con movimiento rápido en la dirección del lanzamiento y, luego, disminuye la velocidad, como si el usuario hubiera empujado un volante y lo hubiera hecho girar.

Sin embargo, 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 de manera correcta. Afortunadamente, Android proporciona clases de ayuda para simular este comportamiento y otros. La clase Scroller es la base para controlar los gestos de lanzamiento de tipo volante.

Para iniciar un lanzamiento, llama a fling() con la velocidad de inicio y los valores de "x" y de "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;
    }
    

Nota: Aunque la velocidad calculada por GestureDetector es físicamente precisa, muchos desarrolladores creen que usar este valor hace que la animación de lanzamiento sea demasiado rápida. Es común dividir la velocidad de "x" y de "y" por un factor de 4 a 8.

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

La mayoría de las vistas pasan la posición de "x" y de "y" del objeto Scroller directamente a scrollTo(). El ejemplo de PieChart es un poco diferente: usa la posición de "y" de desplazamiento actual para establecer el ángulo de rotación del gráfico.

Kotlin

    scroller.apply {
        if (!isFinished) {
            computeScrollOffset()
            setPieRotation(currY)
        }
    }
    

Java

    if (!scroller.isFinished()) {
        scroller.computeScrollOffset();
        setPieRotation(scroller.getCurrY());
    }
    

La clase Scroller calcula las posiciones de desplazamiento, pero no aplica automáticamente esas posiciones a tu vista. Es tu responsabilidad asegurarte de obtener y aplicar nuevas coordenadas con la frecuencia suficiente para que la animación de desplazamiento sea natural. Existen dos maneras de hacerlo:

  • Llama a postInvalidate() después de llamar a fling(), con el fin de forzar un nuevo diseño. Esta técnica requiere que calcules los desplazamientos en onDraw() y llames a postInvalidate() cada vez que cambie el desplazamiento.
  • Configura un valor de ValueAnimator para proporcionar una animación durante la duración del desplazamiento, y agrega un objeto de escucha para procesar actualizaciones de la animación llamando a addUpdateListener().

El ejemplo de PieChart utiliza el segundo enfoque. Esta técnica es un poco más compleja de configurar, pero funciona mejor con el sistema de animación y no requiere una posiblemente innecesaria invalidación de la vista. El inconveniente es que ValueAnimator no está disponible en versiones anteriores a API nivel 11, por lo que esta técnica no se puede usar en dispositivos con versiones de Android anteriores a la versión 3.0.

Nota: Puedes usar ValueAnimator en aplicaciones orientadas a niveles de API inferiores. Solo debes verificar el nivel de la API actual en el tiempo de ejecución y omitir las llamadas al sistema de animación de vista si el nivel actual es inferior a 11.

Kotlin

    private val scroller = Scroller(context, null, true)
    private val scrollAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
        addUpdateListener {
            if (scroller.isFinished) {
                scroller.computeScrollOffset()
                setPieRotation(scroller.currY)
            } else {
                cancel()
                onScrollFinished()
            }
        }
    }
    

Java

    scroller = new Scroller(getContext(), null, true);
    scrollAnimator = ValueAnimator.ofFloat(0,1);
    scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            if (!scroller.isFinished()) {
                scroller.computeScrollOffset();
                setPieRotation(scroller.getCurrY());
            } else {
                scrollAnimator.cancel();
                onScrollFinished();
            }
        }
    });
    

Cómo hacer que tus transiciones sean naturales

Los usuarios esperan que una IU moderna haga una transición natural de un estado a otro. Los elementos de la IU deben desvanecerse, en lugar de aparecer y desaparecer. Los movimientos deben comenzar y terminar de forma natural, y no iniciarse y detenerse abruptamente. El marco de trabajo de la animación de propiedades de Android, presentado en Android 3.0, facilita las transiciones naturales.

Para usar el sistema de animación, cada vez que cambie una propiedad que afectará la apariencia de tu vista, no la cambies directamente. En su lugar, usa ValueAnimator para hacer el cambio. En el siguiente ejemplo, cuando se modifica el sector circular seleccionado en PieChart, el gráfico completo gira para que el puntero de selección se centre en el sector seleccionado. 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, "PieRotation", 0).apply {
        setIntValues(targetAngle)
        duration = AUTOCENTER_ANIM_DURATION
        start()
    }
    

Java

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

Si el valor que quieres cambiar es una de las propiedades de View base, hacer la animación es aún más fácil, porque las vistas tienen un ViewPropertyAnimator que está optimizado para la animación simultánea de múltiples propiedades. Por ejemplo:

Kotlin

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

Java

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