Como tornar a visualização interativa

Desenhar uma IU é apenas uma parte da criação de uma visualização personalizada. Também é necessário fazer com que a visualização responda à entrada do usuário de uma maneira parecida com a ação real que você está imitando. Os objetos precisam sempre agir da mesma maneira que os objetos reais. Por exemplo, as imagens não podem simplesmente sumir de um lugar e reaparecer em outro, porque os objetos no mundo real não fazem isso. Em vez disso, as imagens precisam se mover de um lugar para outro.

Os usuários também percebem comportamentos ou aparências sutis em uma interface e reagem melhor às sutilezas que imitam o mundo real. Por exemplo, quando o usuário move um objeto da IU, ele precisa sentir o atrito inicial que atrasa o movimento e, no fim, o impulso que prolonga o movimento da rolagem rápida.

Esta lição mostra como usar os recursos do framework do Android para adicionar esses comportamentos do mundo real à visualização personalizada.

Além desta lição, você pode encontrar mais informações sobre o assunto em Eventos de entrada e Animação de propriedade.

Gerenciar gestos de entrada

Como muitos outras frameworks de IU, o Android é compatível com um modelo de evento de entrada. As ações do usuário são transformadas em eventos que acionam callbacks, e é possível modificar os callbacks para personalizar o modo como o aplicativo responde ao usuário. O evento de entrada mais comum no sistema Android é o toque, que aciona onTouchEvent(android.view.MotionEvent). Modifique esse método para gerenciar o evento:

Kotlin

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

Java

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

Os eventos de toque por si só não são muito úteis. As UIs de toque modernas definem interações em gestos, como tocar, puxar, empurrar, lançar e aplicar zoom. Para converter eventos de toque brutos em gestos, o Android oferece o GestureDetector.

Crie um GestureDetector passando uma instância de uma classe que implementa GestureDetector.OnGestureListener. Se você quiser apenas processar apenas alguns gestos, poderá estender GestureDetector.SimpleOnGestureListener em vez de implementar a interface GestureDetector.OnGestureListener. Por exemplo, esse código cria uma classe que estende GestureDetector.SimpleOnGestureListener e modifica 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());
    

Se você usa ou não GestureDetector.SimpleOnGestureListener, implemente sempre um método onDown() que retorna true. Essa etapa é necessária porque todos os gestos começam com uma mensagem onDown(). Se você retornar false de onDown(), como GestureDetector.SimpleOnGestureListener faz, o sistema presumirá que você quer ignorar o restante do gesto, e os outros métodos de GestureDetector.OnGestureListener nunca serão chamados. A única vez em que será necessário retornar false de onDown() é se você realmente quiser ignorar um gesto inteiro. Depois de implementar GestureDetector.OnGestureListener e criar uma instância de GestureDetector, você poderá usar seu GestureDetector para interpretar os eventos de toque recebidos em 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;
    }
    

Quando você transmite onTouchEvent() a um evento de toque que ele não reconhece como parte de um gesto, ele retorna false. Você pode, então, executar seu próprio código personalizado de detecção de gestos.

Criar movimento fisicamente plausível

Os gestos são uma maneira poderosa de controlar dispositivos touchscreen, mas podem ser pouco intuitivos e difíceis de lembrar, a menos que produzam resultados fisicamente plausíveis. Um bom exemplo disso é o gesto de rolagem rápida, em que o usuário move um dedo rapidamente pela tela e o levanta. Esse gesto faz sentido se a IU responde movendo-se rapidamente na direção da rolagem, depois diminuindo a velocidade, como se o usuário tivesse girado uma roda.

No entanto, simular a sensação de uma roda não é fácil. É necessário usar muita física e matemática para que o modelo da roda funcione corretamente. Felizmente, o Android oferece classes auxiliares para simular esse e outros comportamentos. A classe Scroller é a base para lidar com gestos de rolagem rápida no estilo de roda.

Para iniciar uma rolagem rápida, chame fling() com a velocidade inicial e os valores mínimo e máximo x e y da rolagem. Para o valor da velocidade, você pode usar o valor calculado para você pelo 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;
    }
    

Observação: embora a velocidade calculada pelo GestureDetector seja fisicamente precisa, muitos desenvolvedores acham que o uso desse valor torna a animação da rolagem muito rápida. É comum dividir a velocidade de x e y por um fator de 4 a 8.

A chamada para fling() configura o modelo de física para o gesto de rolagem rápida. Depois, é necessário atualizar o Scroller chamando Scroller.computeScrollOffset() em intervalos regulares. computeScrollOffset() atualiza o estado interno do objeto Scroller lendo o horário atual e usando o modelo de física para calcular a posição x e y naquele momento. Chame getCurrX() e getCurrY() para recuperar esses valores.

A maioria das visualizações transmite a posição x e y do objeto Scroller diretamente para scrollTo(). O exemplo do PieChart é um pouco diferente: ele usa a posição atual de rolagem y para definir o ângulo de rotação do gráfico.

Kotlin

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

Java

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

A classe Scroller calcula as posições de rolagem, mas não as aplica automaticamente à visualização. É de sua responsabilidade receber e aplicar novas coordenadas com frequência suficiente para tornar a animação de rolagem suave. Há duas maneiras de fazer isso:

  • Chame postInvalidate() depois de chamar fling(), para forçar que seja desenhado novamente. Essa técnica exige que você calcule os deslocamentos de rolagem em onDraw() e chame postInvalidate() sempre que o deslocamento de rolagem for alterado.
  • Configure um ValueAnimator para animar a duração da rolagem rápida e adicione um listener para processar atualizações de animação chamando addUpdateListener().

O exemplo do PieChart usa a segunda abordagem. Essa técnica é um pouco mais complexa de configurar, mas trabalha em mais sintonia com o sistema de animação e não requer invalidação potencialmente desnecessária de visualizações. A desvantagem é que o ValueAnimator não está disponível antes da API de nível 11. Portanto, essa técnica não pode ser usada em dispositivos com versões do Android anteriores à 3.0.

Observação: é possível usar o ValueAnimator em aplicativos voltados para níveis de API mais baixos. Verifique o nível atual da API no tempo de execução e omita as chamadas para o sistema de animação de visualização se o nível atual for menor que 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();
            }
        }
    });
    

Tornar suas transições suaves

Os usuários esperam que uma IU moderna faça transições entre estados de modo suave. Os elementos da IU esmaecem, em vez de aparecer e desaparecer. Os movimentos começam e terminam com leveza, em vez de começar e parar abruptamente. O framework de animação de propriedade do Android, introduzido no Android 3.0, facilita as transições suaves.

Para usar o sistema de animação, sempre que uma propriedade mudar e afetar a aparência da sua visualização, não a altere diretamente. Em vez disso, use ValueAnimator para fazer a mudança. No exemplo a seguir, modificar a parte selecionada no PieChart faz com que o gráfico inteiro gire, para que o ponteiro da seleção seja centralizado na parte selecionada. O ValueAnimator muda a rotação durante um período de várias centenas de milissegundos em vez de definir imediatamente o novo valor de rotação.

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();
    

Se o valor que você quer mudar for uma das propriedades básicas da View, fazer a animação será ainda mais fácil, porque as visualizações têm um ViewPropertyAnimator integrado que é otimizado para animação simultânea de várias propriedades. Por exemplo:

Kotlin

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

Java

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