MotionLayout: as animações são melhores, menos código


O Google continua a melhorar nossas vidas, lançando novas bibliotecas e APIs úteis. Entre os quais estava o novo MotionLayout. Dada a abundância de animações em nossos aplicativos, meu colega Cedric Holtz implementou imediatamente a animação mais importante de nosso aplicativo - votar em namoro - usando a nova API, economizando uma quantidade enorme de código. Eu compartilho a tradução do artigo dele.

A conferência Google I / O 2019 terminou recentemente, na qual foram anunciadas atualizações e as mais recentes melhorias em nosso amado SDK. Pessoalmente, fiquei particularmente interessado na apresentação de Nicholas Road e John Hoford sobre a funcionalidade futura do ConstraintLayout. Mais especificamente, sobre sua expansão na forma de MotionLayout.

Após o lançamento da versão beta, eu queria implementar uma animação de namoro com base nesta biblioteca.

Primeiro, vamos definir os termos:

"MotionLayout é um ConstraintLayout que permite animar layouts entre diferentes estados." - Documentação


Se você não leu uma série de artigos de Nicholas Road que explica as principais idéias do MotionLayout, eu recomendo a leitura.

Então, com a introdução feita, agora vamos ver o que queremos obter:



Pilha de cartões


Mostramos o mapa deslocado


Para começar, adicione MotionLayout ao diretório de layout, que até agora contém apenas um cartão superior:

<androidx.constraintlayout.motion.widget.MotionLayout android:id="@+id/motionLayout"     xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent"     app:layoutDescription="@xml/scene_swipe"     app:motionDebug="SHOW_ALL">     <FrameLayout         android:id="@+id/topCard"         android:layout_width="0dp"         android:layout_height="0dp" /> </androidx.constraintlayout.motion.widget.MotionLayout> 

Preste atenção a esta linha: app: motionDebug = "SHOW_ALL". Ele nos permite exibir informações de depuração, a trajetória dos objetos, os estados com o início e o final da animação, bem como o progresso atual. A linha ajuda muito na depuração, mas não esqueça de excluí-la antes de enviá-la ao produto: não há lembrete disso.

Como você pode ver, não definimos nenhuma restrição para a exibição aqui. Eles serão tirados da cena (MotionScene), que agora definimos.

Vamos começar definindo o estado inicial: um cartão fica no centro da tela, com recuos ao redor.

 <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto">    <ConstraintSet android:id="@+id/rest">        <Constraint            android:id="@id/topCard"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:layout_marginBottom="50dp"            android:layout_marginEnd="50dp"            android:layout_marginStart="50dp"            android:layout_marginTop="50dp"            app:layout_constraintEnd_toEndOf="parent"            app:layout_constraintStart_toStartOf="parent">    </ConstraintSet> </MotionScene> 

Adicione conjuntos de restrições (ConstraintSet) e passe. Eles refletirão o estado do cartão superior quando ele for completamente deslocado para a esquerda ou direita. Queremos que o mapa pare antes de desaparecer da tela para mostrar uma bela animação que confirma nossa decisão.

 <ConstraintSet    android:id="@+id/pass"    app:deriveConstraintsFrom="@+id/rest">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="200dp"        android:layout_marginStart="50dp"        android:layout_marginTop="20dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> <ConstraintSet    android:id="@+id/like"    app:deriveConstraintsFrom="@id/rest">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="50dp"        android:layout_marginStart="200dp"        android:layout_marginTop="20dp"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> 

Adicione os dois conjuntos de restrições à cena anterior. Eles são quase idênticos, apenas espelhados nos dois lados da tela.

Agora temos três conjuntos de restrições - iniciar, curtir e passar. Vamos definir a transição entre esses estados.

Para fazer isso, adicione uma transição à esquerda para deslizar e a outra à direita para deslizar.

 <Transition    app:constraintSetEnd="@+id/pass"    app:constraintSetStart="@+id/rest"    app:duration="300">    <OnSwipe        app:dragDirection="dragLeft"        app:onTouchUp="autoComplete"        app:touchAnchorId="@id/topCard"        app:touchAnchorSide="left"        app:touchRegionId="@id/topCard" /> </Transition> <Transition    app:constraintSetEnd="@+id/like"    app:constraintSetStart="@+id/rest"    app:duration="300">    <OnSwipe        app:dragDirection="dragRight"        app:onTouchUp="autoComplete"        app:touchAnchorId="@+id/topCard"        app:touchAnchorSide="right"        app:touchRegionId="@id/topCard" /> </Transition> 

Assim, para o cartão superior, definimos a animação de furto para a esquerda e o mesmo - espelho para o furto para a direita.

Essas propriedades ajudarão a melhorar a interação com a nossa cena:

  • touchRegionId: como adicionamos preenchimento ao redor do mapa, precisamos garantir que o toque seja reconhecido apenas na área do próprio mapa, e não em todo o MotionLayout. Isso pode ser feito usando touchRegionId.
  • onTouchUp: o que acontecerá com a animação após o lançamento do cartão? Ele deve seguir em frente ou retornar ao seu estado inicial, para que o preenchimento automático seja aplicável.

Vamos ver o que aconteceu:



O mapa sai automaticamente da tela


Agora, trabalharemos na animação, que começará quando o mapa sair da tela.

Adicionamos mais dois conjuntos de ConstraintSet para cada estado final de nossas animações: o mapa deixa a tela esquerda e direita.

Nos exemplos a seguir, mostrarei como criar o estado similar e o estado de passagem repetirá o mesmo espelhado. Um exemplo de trabalho pode ser visto completamente no repositório .

 <ConstraintSet android:id="@+id/offScreenLike">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="50dp"        android:layout_marginTop="20dp"        app:layout_constraintStart_toEndOf="parent"        app:layout_constraintWidth_percent="0.7" />   </ConstraintSet> 

Agora, como no exemplo anterior, você precisa determinar a transição do estado de furto para o estado final. A transição deve funcionar automaticamente imediatamente após a animação do furto. Isso pode ser feito usando a autoTransition:

 <Transition app:autoTransition="animateToEnd" app:constraintSetEnd="@+id/offScreenLike" app:constraintSetStart="@+id/like" app:duration="150" /> 

Agora temos um cartão magnético que pode ser deslizado da tela!



Animação do mapa inferior



Agora vamos fazer a carta de baixo para criar a ilusão de infinito do baralho.

Adicione mais um mapa ao layout, semelhante ao primeiro:

 <FrameLayout    android:id="@+id/bottomCard"    android:layout_width="0dp"    android:layout_height="0dp"    android:background="@color/colorAccent" /> 

Altere o XML para definir as restrições que se aplicam a este mapa em cada estágio da animação:

 <ConstraintSet android:id="@id/rest">    <!-- ... -->    <Constraint android:id="@id/bottomCard">        <Layout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:layout_marginBottom="50dp"            android:layout_marginEnd="50dp"            android:layout_marginStart="50dp"            android:layout_marginTop="50dp" />        <Transform            android:scaleX="0.90"            android:scaleY="0.90" />    </Constraint> </ConstraintSet> <ConstraintSet    android:id="@+id/offScreenLike"    app:deriveConstraintsFrom="@id/like">    <!-- ... -->    <Constraint android:id="@id/bottomCard">        <Transform            android:scaleX="1"            android:scaleY="1" />    </Constraint> </ConstraintSet> 

Para fazer isso, podemos usar a conveniente propriedade ConstraintSet.

Por padrão, cada novo conjunto recebe atributos do MotionLayout pai. Mas, usando o sinalizador deriveConstraintsFrom, você pode definir outro pai para o nosso conjunto. Deve-se ter em mente que, se definirmos restrições usando a marca de restrição, redefiniremos todas as restrições do conjunto pai. Para evitar isso, você pode definir atributos específicos nas tags para que apenas elas sejam substituídas.



No nosso caso, isso significa que, no conjunto de passes, não definimos a tag Layout, mas a copiamos do pai. No entanto, substituímos o Transform, portanto, substituímos todos os atributos especificados na tag Transform por nossos próprios, nesse caso, uma alteração na escala.

É fácil usar o MotionLayout para adicionar um novo elemento e integrá-lo perfeitamente às animações de nossa cena.



Tornando a animação infinita



Depois que a animação é concluída, o cartão superior não pode ser retirado, porque agora se tornou um cartão inferior. Para obter uma animação infinita, você precisa trocar os cartões.

No começo, eu queria fazer isso com uma nova transição:

 <Transition    app:autoTransition="jumpToEnd"    app:constraintSetEnd="@+id/rest"    app:constraintSetStart="@+id/offScreenLike"    app:duration="0" /> 



A animação inteira é reproduzida como deveria. Agora temos uma pilha de cartões que você pode passar sem parar!

Depois de um pequeno golpe, notei algo. A transição para o final da animação do baralho pára quando você toca no cartão. Mesmo que a duração da animação seja zero, ela ainda pára, o que é ruim.



Consegui vencer de apenas uma maneira - alterando programaticamente a transição ativa no MotionLayout.

Para fazer isso, definiremos um retorno de chamada após a conclusão da animação. Assim que offScreenLike e offScreenPass forem concluídos, basta redefinir a transição de volta ao estado de repouso e zerar o progresso.

 motionLayout.setTransitionListener(object : TransitionAdapter() {    override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {        when (currentId) {            R.id.offScreenPass,            R.id.offScreenLike -> {                motionLayout.progress = 0f                motionLayout.setTransition(R.id.rest, R.id.like)            }        }    }   }) 

Não importa qual transição definimos, passamos ou gostamos, quando deslizamos o dedo, passamos para a desejada.



Parece o mesmo, mas a animação não para! Vamos seguir em frente!

Ligação de dados


Crie dados de teste para exibir nos mapas. Por enquanto, nos limitaremos a alterar a cor de fundo de cada cartão.

Criamos um ViewModel com um método svayp que substitui apenas novos dados. Ligue-o à Activity desta maneira:

 val viewModel = ViewModelProviders .of(this) .get(SwipeRightViewModel::class.java) viewModel .modelStream .observe(this, Observer { bindCard(it) }) motionLayout.setTransitionListener(object : TransitionAdapter() { override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) { when (currentId) { R.id.offScreenPass, R.id.offScreenLike -> { motionLayout.progress = 0f motionLayout.setTransition(R.id.rest, R.id.like) viewModel.swipe() } } } }) 

Resta informar o ViewModel sobre a conclusão da animação de furto e atualizará os dados exibidos no momento.



Ícones pop-up


Adicione duas visualizações que, quando deslizadas, aparecem em um dos lados da tela (apenas uma é mostrada abaixo, a segunda é espelhada).

 <ImageView android:id="@+id/likeIndicator" android:layout_width="0dp" android:layout_height="0dp" /> 

Agora, para os mapas, você precisa definir os estados de animação com essas visualizações.

 <ConstraintSet android:id="@id/rest">    <!-- ... -->    <Constraint android:id="@+id/like">        <Layout            android:layout_width="40dp"            android:layout_height="40dp"            app:layout_constraintBottom_toBottomOf="parent"            app:layout_constraintStart_toEndOf="parent"            app:layout_constraintTop_toTopOf="parent" />        <Transform            android:scaleX="0.5"            android:scaleY="0.5" />        <PropertySet android:alpha="0" />    </Constraint>   </ConstraintSet> <ConstraintSet    android:id="@+id/like"    app:deriveConstraintsFrom="@id/rest">    <!-- ... -->    <Constraint android:id="@+id/like">        <Layout            android:layout_width="100dp"            android:layout_height="100dp"            app:layout_constraintBottom_toBottomOf="@id/topCard"            app:layout_constraintEnd_toEndOf="@id/topCard"            app:layout_constraintStart_toStartOf="@id/topCard"            app:layout_constraintTop_toTopOf="@id/topCard" />        <Transform            android:scaleX="1"            android:scaleY="1" />        <PropertySet android:alpha="1" />    </Constraint> </ConstraintSet> 

Não há necessidade de definir restrições nas animações que vão além da tela, pois elas são herdadas dos pais. E no nosso caso, este é um estado de furto.

É tudo o que precisamos fazer. Agora você pode adicionar componentes às cadeias de animação com muita facilidade.



Execute a animação programaticamente



Podemos criar dois botões nos cartões para que o usuário possa não apenas deslizar, mas controlar usando os botões.

Cada botão inicia a mesma animação que o furto.

Como de costume, assine cliques no botão e inicie a animação diretamente no objeto MotionLayout:

 likeButton.setOnClickListener {    motionLayout.transitionToState(R.id.like) } passButton.setOnClickListener {    motionLayout.transitionToState(R.id.pass) } 

Precisamos adicionar botões aos cartões superior e inferior para que a animação seja reproduzida continuamente. No entanto, para o mapa inferior, a inscrição por clique não é necessária, porque não está visível ou o mapa superior é animado e não queremos interrompê-lo.



Outro ótimo exemplo de como o MotionLayout lida com alterações de estado para nós. Vamos diminuir a animação um pouco:



Veja a transição que o MotionLayout realiza quando o passe é substituído. A magia!

Passe o mapa ao longo da curva


Suponha que gostemos se o mapa se mover não em uma linha reta, mas em uma curva (para ser honesto, eu só queria tentar fazer isso).

Então você precisa definir KeyPosition para o movimento nas duas direções, para que o caminho do movimento seja curvado por um arco.

Adicione isso à cena do movimento:

 <Transition    app:constraintSetEnd="@+id/like"    app:constraintSetStart="@+id/rest"    app:duration="300">    <!-- ... -->    <KeyFrameSet>        <KeyPosition            app:drawPath="path"            app:framePosition="50"            app:keyPositionType="pathRelative"            app:motionTarget="@id/topCard"            app:percentX="0.5"            app:percentY="-0.1" />           </KeyFrameSet> </Transition> 


Agora o mapa se move ao longo de um caminho curvo não-banal. Magicamente!

Conclusão


Quando você compara a quantidade de código que recebi ao criar essas animações com nossa implementação atual de animações semelhantes na produção, o resultado é impressionante.

O MotionLayout manipula imperceptivelmente o cancelamento de transições (por exemplo, quando tocado), criando cadeias de animação, alterando propriedades durante as transições e muito mais. Essa ferramenta altera tudo fundamentalmente, simplificando bastante a lógica da interface do usuário.

Vale a pena trabalhar mais algumas coisas (principalmente desabilitar animações e rolagem bidirecional no RecyclerView), mas tenho certeza de que isso é solucionável.

Lembre-se de que a biblioteca ainda está no status beta, mas já abre muitas oportunidades interessantes para nós. Esperamos ansiosamente o lançamento do MotionLayout, que, tenho certeza, será útil mais de uma vez no futuro. Você pode ver o aplicativo totalmente funcional deste artigo no repositório .

PS: e como eles me deram a palavra como tradutor, nossa equipe do Android tem um lugar para um desenvolvedor . Obrigado pela atenção.

Source: https://habr.com/ru/post/pt458854/


All Articles