Este tutorial faz parte do curso Android avançado em Kotlin. Você obterá o máximo valor deste curso se trabalhar com os tutoriais em sequência, mas isso não é obrigatório. Todos os tutoriais do curso estão listados na página de destino Android avançado em Kotlin.

Introdução

A animação é uma ferramenta poderosa para ajudar os usuários a entender uma tela potencialmente complexa e confusa de informações. Quando um único item é atualizado, animar essa mudança pode ajudar o usuário a entender o que aconteceu. Quando muitos itens mudam, as animações podem ajudar a fazer a transição do usuário de uma IU para a próxima, para que eles entendam o contexto e as implicações das mudanças.

Existem muitos tipos diferentes de animações que podem ser usados ​​em interfaces de usuário. Os itens podem esmaecer à medida que aparecem, esmaecer quando desaparecem, mover-se para a tela ou fora dela, ou as formas geométricas podem se transformar de maneiras interessantes. As animações podem ser executadas sozinhas, fornecendo movimento a um único objeto à medida que ele muda de estado, ou podem ser executadas junto com outras animações, pois muitas alterações acontecem uma após a outra ou em paralelo.

O Android oferece muitos recursos para animar objetos de IU A abordagem que você usa e as APIs ou ferramentas usadas para criar essas animações dependem do efeito que você está tentando obter. Este tutorial irá mostrar como criar animações de propriedade, usando ObjectAnimator, que são os blocos de construção básicos da maioria das animações Android. As animações de propriedade são usadas para animar (ou mudar ao longo do tempo) o valor de uma propriedade em um objeto, geralmente um objeto de IU como uma vista Android.

O que você construirá

Neste tutorial, você vai construir um aplicativo que anime estrelas na tela alterando várias propriedades de View que controlam a posição, tamanho, rotação e translucidez. Você começará com a IU básica do aplicativo, um conjunto de botões que, quando pressionados, animarão a estrela, como visto aqui.

Cada etapa do tutorial criará o código para ativar um dos botões na IU:

Ao longo do caminho, você aprenderá novas maneiras de fazer animações mais complexas, bem como conceitos em Kotlin para tornar o código mais elegante e conciso.

O que você aprenderá

Este tutorial é focado na animação de propriedades. Os detalhes da IU já foram feitos para você, pois estão fora do escopo deste laboratório.

O que você precisa

Obtenha o código

Nesta etapa, você baixa o código de todo o tutorial e executa um aplicativo de exemplo simples.

$ git clone https://github.com/googlecodelabs/android-kotlin-animation-property-animation


Alternativamente, você pode baixar o repositório como um arquivo Zip:

Baixar Zip

  1. Descompacte o código
  2. Abra o projeto no Android Studio versão 3.5 ou mais recente.

Execute o código

  1. Crie e execute o aplicativo, que se parece com isto:

Como este laboratório se concentra em técnicas de animação, você não vai construir a IU que o aplicativo usa. Mas você deve saber o que foi feito para você.

Etapa 1: Explorar o arquivo de layout da IU

  1. Em seu projeto Android Studio, navegue até activity_main.xml. Encontre o recipiente de nível superior que é um ConstraintLayout. Dentro desse contêiner, observe seis botões; você conectará esses botões para clicar nos ouvintes para iniciar as animações.
  2. Encontre o FrameLayout, um ViewGroup recipiente que contém um único ImageView. Você pode pensar neste FrameLayout como o fundo em branco (o céu noturno, se preferir) no qual pintará suas animações, usando ImageViews. O ImageView existe para conter o gráfico da estrela usado para demonstrar a maioria das animações neste tutorial.
  3. Agora clique em cada um dos botões: Observe como cada um deles faz... absolutamente nada.

Etapa 2: Conheça o código de atividade

  1. Mude para MainActivity.kt no editor. Você pode ver que parte do código já foi escrito para você. Especificamente, existem variáveis ​​lateinit para manter as vistas que são referenciadas no código.
lateinit var star: ImageView
lateinit var rotateButton: Button
lateinit var translateButton: Button
lateinit var scaleButton: Button
lateinit var fadeButton: Button
lateinit var colorizeButton: Button
lateinit var showerButton: Button
  1. Você também pode ver que essas variáveis ​​são inicializadas com os valores apropriados em onCreate().
star = findViewById(R.id.star)
rotateButton = findViewById<Button>(R.id.rotateButton)
translateButton = findViewById<Button>(R.id.translateButton)
scaleButton = findViewById<Button>(R.id.scaleButton)
fadeButton = findViewById<Button>(R.id.fadeButton)
colorizeButton = findViewById<Button>(R.id.colorizeButton)
showerButton = findViewById<Button>(R.id.showerButton)
  1. A seguir, você verá cinco métodos que serão chamados pelos ouvintes para executar a funcionalidade dos vários botões (rotater(), translater(), etc.). E você verá que essas funções estão vazias; é aqui que você escreverá seu código nas etapas a seguir.
  1. Finalmente, você pode ver o resto do código em onCreate(), no qual você configura ouvintes onClick para cada um dos botões, chamando as funções (atualmente) vazias.
rotateButton.setOnClickListener {
    rotater()
}

translateButton.setOnClickListener {
    translater()
}

scaleButton.setOnClickListener {
    scaler()
}

fadeButton.setOnClickListener {
    fader()
}

colorizeButton.setOnClickListener {
    colorizer()
}

showerButton.setOnClickListener {
    shower()
}

Nesta etapa, você implementará a função rotater() para o tratador de clique rotateButton, que fará com que a estrela gire em um círculo.

Etapa 1: crie o animador

  1. Dentro da função rotater(), crie uma animação que gira a ImageView contendo a estrela de um valor de -360 a 0. Isso significa que a vista e, portanto, a estrela dentro dele, girará em um círculo completo (360 graus) em torno de seu centro.
val animator = ObjectAnimator.ofFloat(star, View.ROTATION, -360f, 0f)

Esta linha de código cria um ObjectAnimator que atua na "estrela" de destino (a instância ImageView que contém o gráfico da estrela). Ele executa uma animação na propriedade ROTATION da estrela. Mudanças nessa propriedade farão com que a estrela gire em torno de seu centro. Existem duas outras propriedades de rotação (ROTATION_X e ROTATION_Y) que giram em torno dos outros eixos (em 3D), mas não são normalmente usadas em animações de IU, uma vez que IUs são normalmente 2D.

A animação vai de um valor inicial de -360 graus a um valor final de 0 graus, o que fará a estrela girar em uma única rotação em torno de seu centro. Observe que o início começa em 0 graus, antes de a animação começar, e então salta imediatamente para -360 graus. Mas, como -360 é visualmente igual a 0 graus, não há nenhuma mudança perceptível quando a animação começa.

  1. Agora você pode executar o aplicativo novamente. Clique no botão ROTATE e você notará... nada. Você configurou a animação, mas ainda não disse para rodar. Vamos fazer isso.

Etapa 2: Run the animation

  1. Abaixo do animator, adicione uma única linha que o inicie.
animator.start()

Agora, se você executar o aplicativo novamente e clicar em ROTATE, verá que a estrela realmente gira em torno de seu centro. Mas isso é muito rápido. Na verdade, ele faz isso em 300 milissegundos, que é a duração padrão de todas as animações na plataforma. 300 milissegundos é um padrão decente para a maioria das animações, mas neste caso, seria bom ter mais tempo para desfrutar da animação.

  1. Altere a propriedade duration do animador para 1000 milissegundos, adicionando uma única linha de código entre as duas linhas anteriores.
val animator = ObjectAnimator.ofFloat(star, View.ROTATION, -360f, 0f)
animator.duration = 1000
animator.start()

Você está quase lá. Se você executar o aplicativo, verá que tem uma bela animação que dura um segundo. Tudo bem, certo?

Etapa 3: evite o movimento descontínuo

Bem... talvez você esteja impaciente, como eu, e executou a animação novamente antes que ela parasse. Você percebeu um salto ao clicar no botão? Isso ocorre porque você sempre redefine para -360 graus no início da animação, independentemente de a estrela estar ou não no meio da animação. Este movimento descontínuo é um exemplo do que é chamado de "jank"; isso causa um fluxo perturbador para o usuário, em vez da experiência suave que você gostaria.

Existem diferentes maneiras de lidar com essa situação (como iniciar a nova animação de qualquer que seja o valor atual). Mas para manter as coisas simples, você apenas impedirá que o usuário clique no botão enquanto a animação estiver em execução, para permitir que eles aproveitem totalmente a animação em processo primeiro.

Os animadores têm um conceito de ouvintes, que chamam de volta o código do usuário para notificar a aplicação de mudanças no estado da animação. Existem retornos de chamada para uma animação começando, terminando, pausando, retomando e repetindo. O que importa aqui são apenas os eventos de início e fim; você gostaria de desativar o botão ROTATE assim que a animação começar e, em seguida, reativá-lo quando a animação terminar.

  1. Adicione um novo objeto AnimatorListenerAdapter ao animador e substitua os métodos onAnimationStart() e onAnimationEnd().
val animator = ObjectAnimator.ofFloat(star, View.ROTATION, -360f, 0f)
animator.duration = 1000
animator.addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationStart(animation: Animator?) {
        rotateButton.isEnabled = false
    }
    override fun onAnimationEnd(animation: Animator?) {
        rotateButton.isEnabled = true
    }
})
animator.start()

Aqui, rotateButton é desativado assim que a animação começa e reativado quando a animação termina. Dessa forma, cada animação é completamente separada de qualquer outra animação de rotação, evitando o problema de reiniciar no meio.

É isso para esta primeira tarefa; agora você tem um aplicativo que pode iniciar animações de rotação na estrela. Leve-o para dar uma volta!

Nesta tarefa, você conectará translateButton a um ouvinte onClick, o que fará com que a estrela se mova para frente e para trás. translateButton chama a função translater(), que está vazia no momento. Vamos preencher isso.

Etapa 1: Crie o animador

  1. Dentro da função translater(), crie uma animação que mova a estrela 200 pixels para a direita e a execute.
val animator = ObjectAnimator.ofFloat(star, View.TRANSLATION_X, 200f)
animator.start()
  1. Execute o aplicativo agora. Quando você clica em TRANSLATE, a estrela se move para a direita... mas não volta para o centro. Se você clicar no botão novamente, ele não se moverá.

    O que está acontecendo?

    Em primeiro lugar, a animação está sendo configurada para rodar apenas em uma direção; ele anima a estrela 200 pixels para a direita... e é isso. Então, se você quiser que ele volte, você vai precisar de algo extra.

    Além disso, as animações subsequentes não parecem fazer nada porque a animação está configurada para rodar para um valor de 200. Depois a animação foi executada, o valor já está em 200, então não há outro lugar para ir.

    Você pode corrigir esses dois problemas usando o conceito de "repetição".
  1. Altere a animação para repetir, reproduzindo de volta à posição inicial. Defina a propriedade repeatCount na animação (que controla quantas vezes ela se repete após a primeira execução), bem como o tipo de repetição (REVERSE ou RESTART para repetir novamente de / para os mesmos valores).
val animator = ObjectAnimator.ofFloat(star, View.TRANSLATION_X, 200f)
animator.repeatCount = 1
animator.repeatMode = ObjectAnimator.REVERSE
animator.start()
  1. Execute o aplicativo novamente e você verá que o botão agora é animado para a direita e para trás. Isto é, a menos que você clique no botão novamente enquanto ele está em execução, o que causa um problema.

    O problema aqui é diferente do que você viu com a animação de rotação. Nessa tarefa, a estrela voltaria ao seu valor inicial para começar a animação novamente. Mas aqui, a animação não se encaixa. Em vez disso, ele começa a animar de onde está, mas não vai tão longe. Por exemplo, se você reiniciá-lo na metade de sua viagem de volta (quando está em 100), ele começará a nova animação de 100... mas ainda assim irá apenas para 200. Portanto, a animação geral é muito mais curta porque começou a partir de um valor maior do que o ponto inicial original de 0.

    O que está acontecendo?

    Há uma diferença sutil entre este animador e o animador usado para a tarefa de rotação. A animação de rotação recebeu os valores inicial e final, por isso sempre executou a animação entre esses dois valores. Aqui, a animação recebe apenas um valor end. Quando a animação começa, ela primeiro consulta o valor atual da propriedade da tradução em star e usa isso como seu valor inicial implícito, animando desse valor para 200. Então, quando você clica em TRANSLATE botão quando a animação está no meio do caminho, ele pega esse valor intermediário como o valor inicial para a nova animação e executa a animação em uma distância menor a partir daí até 200.

Etapa 2: evite reinicializações enquanto a animação estiver em execução

Você vai consertar isso de forma semelhante a como fez para a animação de rotação, desativando o translateButton durante a animação, de modo que a animação pare em 0 antes de poder ser executada novamente.

Como esta é a segunda vez que você está escrevendo um código muito semelhante (adicionando um ouvinte para habilitar / desabilitar um botão), você deve refatorar esse código em uma função separada que você usará em todos os lugares que precisar.

  1. Crie uma função chamada disableViewDuringAnimation(), que recebe uma View e um Animator, e use o código que você já escreveu anteriormente em rotater() para criar o corpo desta função.
private fun disableViewDuringAnimation(view: View,
                                       animator: ObjectAnimator) {
    animator.addListener(object : AnimatorListenerAdapter() {
        override fun onAnimationStart(animation: Animator?) {
            view.isEnabled = false
        }

        override fun onAnimationEnd(animation: Animator?) {
            view.isEnabled = true
        }
    })
}
  1. Agora chame este método em translater() erotater() para desativar seus botões durante suas respectivas animações. Além disso, remova o código que define o ouvinte de clique na função rotater().
private fun rotater() {
    val animator = ObjectAnimator.ofFloat(star, View.ROTATION,
                                          -360f, 0f)
    animator.duration = 1000
    disableViewDuringAnimation(rotateButton, animator)
    animator.start()
}

private fun translater() {
    val animator = ObjectAnimator.ofFloat(star, View.TRANSLATION_X,
                                          200f)
    animator.repeatCount = 1
    animator.repeatMode = ObjectAnimator.REVERSE
    disableViewDuringAnimation(translateButton, animator)
    animator.start()
}
  1. Execute o aplicativo novamente. Agora você deve ver que a rotação funciona como antes, e que a tradução também ativa / desativa seu botão devido à funcionalidade View desativada que você adicionou ao ouvinte do animador.

Etapa 3: Refatorar em uma função de extensão

Como uma etapa bônus, aproveite os recursos de linguagem do Kotlin usando funções de extensão.

  1. Altere a função disableViewDuringAnimation() para ser uma função de extensão em ObjectAnimator. Isso torna a função mais concisa para chamar, pois elimina um parâmetro. Também torna o código um pouco mais natural de ler, colocando a funcionalidade relacionada ao animador diretamente no ObjectAnimator.
private fun ObjectAnimator.disableViewDuringAnimation(view: View) {
    addListener(object : AnimatorListenerAdapter() {
        override fun onAnimationStart(animation: Animator?) {
            view.isEnabled = false
        }

        override fun onAnimationEnd(animation: Animator?) {
            view.isEnabled = true
        }
    })
}
  1. Modifique o código em translater() para chamar esta função de extensão.
private fun translater() {
    val animator = ObjectAnimator.ofFloat(star, View.TRANSLATION_X,
                                          200f)
    animator.repeatCount = 1
    animator.repeatMode = ObjectAnimator.REVERSE
    animator.disableViewDuringAnimation(translateButton)
    animator.start()
}
  1. Faça a mesma alteração em rotater(), chamando a nova função de extensão.
private fun rotater() {
    val animator = ObjectAnimator.ofFloat(star, View.ROTATION,
                                          -360f, 0f)
    animator.duration = 1000
    animator.disableViewDuringAnimation(rotateButton)
    animator.start()
}

Agora você vai preencher o corpo da função scaler(). Desta vez, você animará duas propriedades em paralelo.

Nas duas etapas anteriores, você estava apenas animando uma propriedade, porque era tudo o que era necessário: girar em torno de um único eixo (o eixo "z", que é perpendicular à tela) e transladar ao longo de um único eixo (o "x" eixo, que corre da esquerda para a direita na tela).

Mas quando um objeto é dimensionado, ele geralmente é dimensionado em x e y simultaneamente, para evitar que pareça "esticado" ao longo de um dos eixos ("espelho divertido" geralmente não é o efeito de se empenhar em design de IU). Portanto, você deve criar uma animação que irá animar as propriedades SCALE_X e SCALE_Y ao mesmo tempo.

Existem algumas maneiras de fazer isso (incluindo usando um AnimatorSet, que você verá na etapa final deste laboratório). Mas uma boa técnica para conhecer usa PropertyValuesHolder, que é um objeto que contém informações sobre uma propriedade e os valores entre os quais essa propriedade deve animar.

Etapa 1: crie uma animação usando PropertyValuesHolder

Nas tarefas anteriores, você forneceu informações para ObjectAnimator sobre a propriedade a ser animada (como TRANSLATE_X) junto com os valores de animação (por exemplo, o valor final de 200f para tradução). Mas, em vez disso, você pode usar um objeto intermediário chamado PropertyValuesHolder para armazenar essas informações e, em seguida, criar um único ObjectAnimator com vários objetos PropertyValuesHolder. Esse único animador executará uma animação em dois ou mais desses conjuntos de propriedades / valores juntos.

  1. Primeiro, crie dois objetos PropertyValuesHolder como as primeiras linhas em scaler().
val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 4f)
val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 4f)

Escalar para um valor de 4f significa que a estrela será escalonada para 4 vezes seu tamanho padrão.

  1. Agora crie um objeto ObjectAnimator, como antes, mas use os objetos scaleX e scaleY que você criou acima para especificar as informações de propriedade / valor.
val animator = ObjectAnimator.ofPropertyValuesHolder(
        star, scaleX, scaleY)

É semelhante aos animadores que você criou anteriormente, mas em vez de definir uma propriedade e um conjunto de valores, usa vários PropertyValuesHolder s que já contêm todas essas informações. O uso de vários objetos PropertyValuesHolder em um único animador fará com que todos sejam animados em paralelo.

Etapa 2: Limpe a animação

Agora você pode concluir o método como fez nas tarefas anteriores, redefinindo o objeto de volta a um estado final razoável e evitando problemas com animações descontínuas.

  1. Tal como acontece com a função translater(), você deseja torná-la uma animação de repetição / reversão para deixar as propriedades SCALE_X e SCALE_Y da estrela em seu padrão valores (1,0) quando a animação é feita. Faça isso definindo os valores de repeatCount e repeatMode apropriados no animador.
animator.repeatCount = 1
animator.repeatMode = ObjectAnimator.REVERSE
  1. Além disso, como com as animações anteriores, chame a função de extensão disableViewDurationAnimation() para desativar scaleButton durante a animação. Adicionar o restante desse código resulta na versão final da função.
private fun scaler() {
    val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 4f)
    val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 4f)
    val animator = ObjectAnimator.ofPropertyValuesHolder(
            star, scaleX, scaleY)
    animator.repeatCount = 1
    animator.repeatMode = ObjectAnimator.REVERSE
    animator.disableViewDuringAnimation(scaleButton)
    animator.start()
}
  1. Execute o aplicativo. Observe que a estrela agora é dimensionada para 4x seu tamanho original... e então retorna ao seu estado original.

Agora, para a fase final das animações básicas, você vai esmaecer a estrela para ficar completamente transparente e, em seguida, voltar a ficar totalmente opaca.

Itens esmaecidos podem ser uma forma muito útil de fazer a transição para dentro ou fora de uma IU. Por exemplo, ao remover um item de uma lista, você pode esmaecer o conteúdo primeiro, antes de fechar a lacuna que ele deixa. Ou, se novas informações aparecerem em uma IU, você pode aparecer gradualmente. Esse efeito não apenas evita o movimento descontínuo, com elementos da IU se encaixando e saindo na frente do usuário, mas ajuda a alertar o usuário de que há uma mudança acontecendo. de apenas remover ou adicionar itens e fazê-los adivinhar o que aconteceu.

O desvanecimento é feito usando a propriedade ALPHA em View.

  1. Defina a função fader() para reduzir a vista a 0 e depois voltar ao seu valor inicial. Este código é essencialmente equivalente ao código de função translater() que você escreveu antes, exceto com uma propriedade e valor final diferentes. Esta é a aparência da função quando é concluída.
private fun fader() {
    val animator = ObjectAnimator.ofFloat(star, View.ALPHA, 0f)
    animator.repeatCount = 1
    animator.repeatMode = ObjectAnimator.REVERSE
    animator.disableViewDuringAnimation(fadeButton)
    animator.start()
}

Uma das coisas poderosas sobre o ObjectAnimatoré que ele pode animar qualquer coisa, desde que haja uma propriedade que o animador possa acessar.

Etapa 1: Anime uma propriedade arbitrária

Aqui está um exemplo simples de animação de uma única propriedade em um objeto. Desta vez, essa propriedade não é um objeto android.util.Property, mas sim uma propriedade exposta por meio de um setter, View.setBackgroundColor(int). Já que você não pode se referir a um objeto android.util.Property diretamente, como você fez antes com ALPHA, etc., você usará a abordagem de passar o nome do propriedade como uma String. O nome é então mapeado internamente para as informações de setter / getter apropriadas no objeto de destino.

Para este exemplo, você preencherá a função colorizer(), que é chamada quando você clica em colorizeButton. Nesta animação, você mudará a cor de fundo do campo estelar de preto para vermelho (e vice-versa).

Primeiro, você precisará de um ObjectAnimator que pode atuar no tipo apropriado. Você poderia usar o método de fábrica ObjectAnimator.ofInt(), uma vez que View.setBackgroundColor(int) leva um int, mas... isso faria dar resultados inesperados.

  1. Na função colorizer(), crie e execute esse animador para ver o problema.
var animator = ObjectAnimator.ofInt(star.parent,
    "backgroundColor", Color.BLACK, Color.RED).start()
  1. Agora execute seu aplicativo e clique em colorizeButton.

Essa demonstração não é chamativa? Na verdade, é um pouco chamativo demais, pois pisca entre muitas cores diferentes no caminho de BLACK a RED. Sem entrar muito em detalhes, o problema é que a animação está interpretando inteiros brutos como cores. A animação entre dois valores inteiros não produz necessariamente o mesmo resultado que a animação entre as cores que esses dois inteiros representam.

Etapa 2: animar cores, não inteiros

O que você precisa, em vez disso, é um animador que saiba como interpretar (e animar entre) os valores das cores, em vez dos inteiros que representam essas cores.

  1. Use um método de fábrica diferente para o animador, ObjectAnimator.ofArgb(). Tente o código novamente, usando este método de fábrica.
var animator = ObjectAnimator.ofArgb(star.parent,
    "backgroundColor", Color.BLACK, Color.RED).start()
  1. Agora execute o aplicativo. Você verá que ele se anima suavemente do preto ao vermelho, sem os estranhos flashes de cores ao longo do caminho.

A outra coisa a notar sobre esta construção do ObjectAnimator é a propriedade: em vez de especificar uma das propriedades de View, como ALPHA, você é simplesmente passando a string "backgroundColor". Quando você faz isso, o sistema procura setters e getters com a grafia exata usando reflexão. Ele armazena em cache as referências a esses métodos e os chama durante a animação, em vez de chamar as funções Property set / get como as animações anteriores faziam.

Etapa 3: Esmaecer [voltar] para preto

Atualmente, você tem a capacidade de animar do preto ao vermelho... e é aí que permanece. Se você clicar no botão novamente, ele será animado novamente, mas sempre termina em vermelho, porque a animação anima explicitamente de preto para um valor final de vermelho.

  1. Altere a animação para demorar um pouco mais para ser executada definindo uma duração explícita e, em seguida, volte a animar para preto. Além disso, desative o botão durante a animação, como fez com as outras animações, chamando a função de extensão criada anteriormente. Aqui está a aparência dessa função completa.
private fun colorizer() {
    var animator = ObjectAnimator.ofArgb(star.parent,
        "backgroundColor", Color.BLACK, Color.RED)
    animator.setDuration(500)
    animator.repeatCount = 1
    animator.repeatMode = ObjectAnimator.REVERSE
    animator.disableViewDuringAnimation(colorizeButton)
    animator.start()
}

Isso é tudo que há para fazer. Esta animação é muito semelhante a todas as outras que você configurou neste laboratório, exceto pelo uso da string "backgroundColor" para o nome da propriedade. Isso não parece muito diferente do que você fez antes, exceto que significa que você pode usar ObjectAnimator para animar literalmente qualquer coisa que tenha um setter / getter. Por exemplo, você poderia ter uma View personalizada com uma propriedade chamada lineLength, que define o comprimento de algum segmento de linha em sua IU (talvez usando código de desenho personalizado em uma substituição onDraw()). Passar lineLength para o construtor do animador resultaria na animação desse comprimento de linha, porque o sistema mapeia essa string para o configurador de propriedade subjacente em seu código de vista personalizada.

Agora, para a etapa final, você criará uma animação um pouco mais envolvente, animando várias propriedades em vários objetos.

Para este efeito, um clique de botão resultará na criação de uma estrela de tamanho aleatório, que será adicionada ao container de fundo, fora da vista do topo daquele container. A estrela começará a cair na parte inferior da tela, acelerando conforme avança. Ao cair, ele girará.

Para esta etapa, você preencherá a função shower() para conectar uma única animação de uma estrela cadente a um único clique do botão SHOWER. Existem alguns novos conceitos aqui, além do que você viu nas etapas anteriores.

Etapa 1: nasce uma estrela

Primeiro, você vai precisar de algumas variáveis ​​locais para manter o estado de que precisará no código seguinte. Especificamente, você precisará de:

  1. Comece a preencher o interior da função shower() com o seguinte código.
val container = star.parent as ViewGroup
val containerW = container.width
val containerH = container.height
var starW: Float = star.width.toFloat()
var starH: Float = star.height.toFloat()
  1. Crie uma nova View para conter o gráfico da estrela. Como a estrela é um ativo VectorDrawable, use um AppCompatImageView, que tem a capacidade de hospedar esse tipo de recurso. Crie a estrela e adicione-a ao recipiente de fundo.
val newStar = AppCompatImageView(this)
newStar.setImageResource(R.drawable.ic_star)
newStar.layoutParams = FrameLayout.LayoutParams(
                           FrameLayout.LayoutParams.WRAP_CONTENT,
                           FrameLayout.LayoutParams.WRAP_CONTENT)
container.addView(newStar)
  1. Execute o aplicativo e clique no botão SHOWER. Você verá a nova estrela que criou no canto superior esquerdo.

Etapa 2: dimensionar e posicionar a estrela

Você ainda não disse a esta imagem onde ser posicionada no contêiner, então ela está posicionada em (0, 0) por padrão. Você corrigirá este posicionamento nesta etapa.

  1. Na função shower(), defina o tamanho da estrela. Modifique a estrela para ter um tamanho aleatório, de 0,1x a 1,6x de seu tamanho padrão. Use este fator de escala para alterar os valores de largura / altura em cache, porque você precisará saber a altura / largura real do pixel para cálculos posteriores.
newStar.scaleX = Math.random().toFloat() * 1.5f + .1f
newStar.scaleY = newStar.scaleX
starW *= newStar.scaleX
starH *= newStar.scaleY

Agora você armazenou em cache a altura / largura em pixels da estrela armazenada em starW e starH:

  1. Agora posicione a nova estrela. Horizontalmente, deve aparecer aleatoriamente em algum lugar da borda esquerda para a borda direita. Este código usa a largura da estrela para posicioná-la da metade da tela à esquerda (-starW / 2) até a metade da tela à direita (com a estrela posicionada em (containerW - starW / 2). O posicionamento vertical da estrela será tratado posteriormente no código de animação real.
newStar.translationX = Math.random().toFloat() *
                       containerW - starW / 2

Etapa 3: crie animadores para rotação e queda das estrelas

Você concluiu a configuração das informações iniciais da estrela; é hora de trabalhar na animação. A estrela deve girar enquanto cai. Você já viu uma maneira de animar duas propriedades juntas, usando PropertyValuesHolder, na tarefa anterior de dimensionamento. Você poderia fazer algo semelhante aqui, exceto que haverá diferentes tipos de movimento, o que é chamado de "interpolação", nessas duas animações. Especificamente, a rotação usará um movimento linear suave (movendo-se a uma taxa constante ao longo de toda a animação de rotação), enquanto a animação de queda usará um movimento acelerado (simulando a gravidade puxando a estrela para baixo a uma taxa constantemente mais rápida). Portanto, você criará dois animadores e adicionará um interpolador a cada um.

  1. Primeiro, crie dois animadores, junto com seus interpoladores.
val mover = ObjectAnimator.ofFloat(newStar, View.TRANSLATION_Y,
                                   -starH, containerH + starH)
mover.interpolator = AccelerateInterpolator(1f)
val rotator = ObjectAnimator.ofFloat(newStar, View.ROTATION,
        (Math.random() * 1080).toFloat())
rotator.interpolator = LinearInterpolator()

A animação mover é responsável por fazer a estrela "cair". Ele anima a propriedade TRANSLATION_Y, semelhante ao que você fez com TRANSLATION_X na tarefa de tradução anterior, mas causando movimento vertical em vez de horizontal. O código é animado de -starH a (containerH + starH), o que efetivamente o coloca fora do recipiente na parte superior e o move até que esteja fora do recipiente na parte inferior, conforme mostrado aqui:

O AccelerateInterpolator "interpolador" que você está definindo na estrela causa um movimento de aceleração suave.

Para a animação de rotação, a estrela irá girar em uma quantidade aleatória entre 0 e 1080 graus (três vezes ao redor). Para o movimento, use um Interpolador Linear, para que a rotação prossiga a uma taxa constante conforme a estrela cai.

Etapa 4: execute as animações em paralelo com AnimatorSet

Agora é hora de colocar esses dois animadores juntos em um único AnimatorSet, que é útil para esta animação um pouco mais complexa envolvendo vários ObjectAnimator s.. AnimatorSet é basicamente um grupo de animações, junto com instruções sobre quando executar essas animações. Ele pode reproduzir animações em paralelo, como você fará aqui, ou sequencialmente (como você pode fazer no exemplo de esmaecimento de lista mencionado anteriormente, onde primeiro você esmaece uma vista e então anima a lacuna resultante fechada). Um AnimatorSet também pode conter outros AnimatorSet s, então você pode criar coreografias hierárquicas muito complexas agrupando animadores nesses conjuntos.

  1. Crie o AnimatorSet e adicione os animadores filhos a ele (junto com informações para reproduzi-los em paralelo). O tempo de animação padrão de 300 milissegundos é muito rápido para aproveitar as estrelas cadentes, então defina a duração para um número aleatório entre 500 e 2.000 milissegundos, para que as estrelas caiam em velocidades diferentes.
val set = AnimatorSet()
set.playTogether(mover, rotator)
set.duration = (Math.random() * 1500 + 500).toLong()
  1. Assim que newStar cair da parte inferior da tela, deve ser removido do recipiente. Configure um ouvinte simples para aguardar o final da animação e remova-o. Em seguida, inicie a animação.
set.addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationEnd(animation: Animator?) {
        container.removeView(newStar)
    }
})
set.start()
  1. Execute seu aplicativo. Você pode clicar no botão SHOWER várias vezes, criando uma nova estrela e uma nova animação a cada vez. Observe que você não teve que desabilitar o botão durante a animação, como fez nas tarefas anteriores, porque desta vez você deseja criar várias animações simultâneas. Não há problema com artefatos de movimento descontínuo porque cada animação é independente das outras e opera em um objeto de destino diferente.

    Você deve ver algo assim:

Parabéns, você construiu com sucesso um aplicativo que executa vários tipos diferentes de animações de propriedade. Animar estrelas pode não ser o tipo de experiência de IU que você deseja em seus aplicativos, mas as ferramentas usadas neste laboratório são exatamente as ferramentas que você deve usar para animar elementos de IU em situações do mundo real. ObjectAnimator, AnimatorSet, LinearInterpolator, PropertyValuesHolder são APIs boas para entender a fim de escrever animações em seu código.

Para obter links para outros tutoriais neste curso, consulte a página inicial de tutoriais do Android avançado em Kotlin.