Tornar uma visualização personalizada interativa

Teste o Compose
O Jetpack Compose é o kit de ferramentas de interface recomendado para Android. Aprenda a trabalhar com layouts no Compose.

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 do mundo real que você está imitando.

Faça com que os objetos do app atue como objetos reais. Por exemplo, não deixe as imagens do app desaparecerem e reaparecerem em outro lugar, porque objetos no mundo real não fazem isso. Em vez disso, mova suas imagens de um lugar para outro.

Os usuários percebem até mesmo comportamentos sutis em uma interface e reagem melhor a sutilezas que imitam o mundo real. Por exemplo, quando os usuários lançam um objeto da interface, dê a eles uma sensação de inércia no início que atrasa o movimento. No final do movimento, dê a eles uma sensação de movimento que transporta o objeto para além da rolagem.

Esta página demonstra como usar os recursos do framework do Android para adicionar esses comportamentos do mundo real à sua visualização personalizada.

Confira mais informações relacionadas em Visão geral de eventos de entrada e Visão geral de animação de propriedades.

Processar 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 se tornam eventos que acionam callbacks, e é possível substituí-los para personalizar a resposta do app 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 processar o evento da seguinte maneira:

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 IUs modernas de toque definem interações em termos de gestos, como tocar, puxar, empurrar, deslizar e aplicar zoom. Para converter eventos de toque brutos em gestos, o Android oferece GestureDetector.

Crie um GestureDetector transmitindo uma instância de uma classe que implementa GestureDetector.OnGestureListener. Se você quiser 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 substitui 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());

Mesmo que você não use GestureDetector.SimpleOnGestureListener, sempre implemente um método onDown() que retorne true. Isso é necessário porque todos os gestos começam com uma mensagem onDown(). Se você retornar false de onDown(), como GestureDetector.SimpleOnGestureListener, o sistema presumirá que você quer ignorar o restante do gesto e que os outros métodos de GestureDetector.OnGestureListener não serão chamados. Retorne false de onDown() apenas se quiser ignorar um gesto inteiro.

Depois de implementar GestureDetector.OnGestureListener e criar uma instância de GestureDetector, é possível usar a 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 a onTouchEvent() um evento de toque que ele não reconhece como parte de um gesto, ele retorna false. Em seguida, você pode 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 contraintuitivos e difíceis de lembrar, a menos que produzam resultados fisicamente plausíveis.

Por exemplo, suponha que você queira implementar um gesto de rolagem rápida horizontal que defina o item desenhado na visualização que gira em torno do eixo vertical. Esse gesto faz sentido se a interface responde se movendo rapidamente na direção da rolagem e desacelerando, como se o usuário empurre uma roda e a faz girar.

A documentação sobre como animar um gesto de rolagem fornece uma explicação detalhada sobre como implementar seu próprio comportamento de rolagem. Mas simular a sensação de um volante não é trivial. É preciso muita física e matemática para que um modelo de volante funcione corretamente. Felizmente, o Android fornece classes auxiliares para simular esse e outros comportamentos. A classe Scroller é a base para processar gestos de rolagem rápida no estilo volante.

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

A chamada para fling() configura o modelo de física para o gesto de rolagem rápida. Em seguida, atualize o Scroller chamando Scroller.computeScrollOffset() em intervalos regulares. computeScrollOffset() atualiza o estado interno do objeto Scroller lendo a hora atual e usando o modelo de física para calcular as posições x e y naquele momento. Chame getCurrX() e getCurrY() para extrair esses valores.

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

Kotlin

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

Java

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

A classe Scroller calcula as posições de rolagem, mas não as aplica automaticamente à visualização. Aplique novas coordenadas com frequência suficiente para que a animação de rolagem pareça suave. Há duas maneiras de fazer isso:

  • Para forçar um redesenho, chame postInvalidate() depois de chamar fling(). Essa técnica exige que você calcule os deslocamentos de rolagem em onDraw() e chame postInvalidate() sempre que o deslocamento de rolagem mudar.
  • 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(). Essa técnica permite animar propriedades de um View.

Suavizar as transições

Os usuários esperam que uma interface moderna faça uma transição suave entre estados: elementos da interface que aparecem e desaparecem em vez de aparecer e desaparecer, e movimentos que começam e terminam sem problemas, em vez de começar e parar abruptamente. O framework de animação de propriedade do Android facilita as transições suaves.

Para usar o sistema de animação, sempre que uma propriedade mudar o que afeta a aparência da visualização, não a mude diretamente. Em vez disso, use ValueAnimator para fazer a mudança. No exemplo abaixo, modificar o componente filho selecionado na visualização faz com que toda a visualização renderizada seja renderizada para que o ponteiro da seleção fique centralizado. 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, "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();

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

Kotlin

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

Java

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