It's happening now, watch the livestream.

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 agir sempre da mesma maneira que os objetos reais. Por exemplo, as imagens não podem desaparecer e reaparecer imediatamente em outro lugar, porque 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.

Construa um GestureDetector passando uma instância de uma classe que implemente GestureDetector.OnGestureListener. Se você quer processar apenas alguns gestos, pode estender o GestureDetector.SimpleOnGestureListener, em vez de implementar a interface GestureDetector.OnGestureListener. Por exemplo, este 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());
    

Quer você use ou não o GestureDetector.SimpleOnGestureListener, implemente sempre um método onDown() que retorne true. Essa etapa é necessária porque todos os gestos começam com uma mensagem onDown(). Se você retornar false de onDown(), como o 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 é necessário retornar false de onDown() é se você realmente quiser ignorar um gesto inteiro. Depois de implementar o GestureDetector.OnGestureListener e criar uma instância do GestureDetector, use o 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 gerenciar gestos de rolagem rápida no estilo da roda.

Para iniciar uma rolagem rápida, chame fling() com a velocidade inicial e os valores mínimo e máximo de x e y dele. Para o valor da velocidade, você pode usar o valor calculado 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 disso, atualize o Scroller chamando Scroller.computeScrollOffset() em intervalos regulares. O computeScrollOffset() atualiza o estado interno do objeto Scroller lendo a hora atual e usando o modelo de física para calcular as posições de x e y naquele momento. Chame getCurrX() e getCurrY() para recuperar esses valores.

A maioria das visualizações passa 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() toda vez 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 do nível 11 da API, portanto, essa técnica não pode ser usada em dispositivos com versões do Android anteriores à 3.0.

Observação: você pode usar o ValueAnimator em aplicativos voltados para níveis mais baixos de API. Verifique o nível atual da API no momento da 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 altera 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();