À medida que seu app é usado, novas informações precisarão ser mostradas na tela enquanto as informações antigas são removidas. Mudar imediatamente o que é mostrado pode ser chocante ou os usuários podem não ver o novo conteúdo na tela. Usar animações pode ser uma forma de apresentar as alterações com mais cuidado e chamar a atenção do usuário para que as atualizações sejam mais aparentes.
Existem três animações comuns a serem usadas para mostrar ou ocultar uma visualização. Você pode usar a animação de revelação circular, uma animação de fading cruzado ou uma animação de virada de cartão.
Criar uma animação de fading cruzado
As animações de fading cruzado (também conhecidas como "dissolução") desvanecem gradualmente uma View
ou um ViewGroup
enquanto mostram outra. Essa animação é útil para situações em que você quer alternar conteúdo ou visualizações no seu app. A animação de fading cruzado mostrada aqui usa ViewPropertyAnimator
, que está disponível para o Android 3.1 (API de nível 12) e versões mais recentes.
Veja um exemplo de fading cruzado de um indicador de progresso para conteúdo de texto.
Criar as visualizações
Em primeiro lugar, você precisa criar as duas visualizações que você quer usar na animação de fading cruzado. O exemplo a seguir cria um indicador de progresso e uma visualização de texto rolável:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView style="?android:textAppearanceMedium"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lorem_ipsum"
android:padding="16dp" />
</ScrollView>
<ProgressBar android:id="@+id/loading_spinner"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
Configurar a animação de fading cruzado
Para configurar a animação de fading cruzado:
- Crie variáveis de membro para as visualizações que você quer usar na animação. Você precisará dessas referências posteriormente para modificar as visualizações durante a animação.
- Para a visualização que será mostrada, defina a visibilidade como
GONE
. Isso evita que ela ocupe espaço no layout e a omite dos cálculos de layout, acelerando o processamento. - Armazene em cache a propriedade de sistema
em uma variável de membro. Essa propriedade define uma duração "curta" padrão para a animação. Essa duração é ideal para animações sutis ou animações que ocorrem com muita frequência.config_shortAnimTime
config_longAnimTime
econfig_mediumAnimTime
também estarão disponíveis se você quiser usá-los.
Veja um exemplo com o layout do snippet de código anterior como a visualização do conteúdo da atividade:
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_crossfade) contentView = findViewById(R.id.content) loadingView = findViewById(R.id.loading_spinner) // Initially hide the content view. contentView.visibility = View.GONE // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) } ... }
Java
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crossfade); contentView = findViewById(R.id.content); loadingView = findViewById(R.id.loading_spinner); // Initially hide the content view. contentView.setVisibility(View.GONE); // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = getResources().getInteger( android.R.integer.config_shortAnimTime); } ... }
Fading cruzado nas visualizações
Agora que as visualizações estão configuradas adequadamente, faça o fading cruzado da seguinte forma:
- Para a visualização que será mostrada, defina o valor Alfa como
0
e a visibilidade comoVISIBLE
. Lembre-se de que ela foi inicialmente definida comoGONE
. Isso torna a visualização visível, mas completamente transparente. - Para a visualização que será mostrada, anime o valor Alfa de
0
a1
. Para a visualização que desaparecerá, anime o valor Alfa de1
a0
. - Usando
onAnimationEnd()
em umAnimator.AnimatorListener
, defina a visibilidade da visualização que estava desaparecendo comoGONE
. Mesmo que o valor Alfa seja0
, definir a visibilidade da visualização comoGONE
impede que ela ocupe espaço de layout e a omite dos cálculos de layout, acelerando o processamento.
O método a seguir mostra um exemplo de como fazer isso:
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... private fun crossfade() { contentView.apply { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. alpha = 0f visibility = View.VISIBLE // Animate the content view to 100% opacity, and clear any animation // listener set on the view. animate() .alpha(1f) .setDuration(shortAnimationDuration.toLong()) .setListener(null) } // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step (it won't // participate in layout passes, etc.) loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration.toLong()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { loadingView.visibility = View.GONE } }) } }
Java
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... private void crossfade() { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. contentView.setAlpha(0f); contentView.setVisibility(View.VISIBLE); // Animate the content view to 100% opacity, and clear any animation // listener set on the view. contentView.animate() .alpha(1f) .setDuration(shortAnimationDuration) .setListener(null); // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step (it won't // participate in layout passes, etc.) loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { loadingView.setVisibility(View.GONE); } }); } }
Criar uma animação de virada de cartão
Viradas de cartão acontecem entre visualizações de conteúdo, mostrando uma animação que emula uma virada de cartão. A animação de virada de cartão mostrada aqui usa FragmentTransaction
, disponível para Android 3.0 (API de nível 11) e versões mais recentes.
Veja a aparência de uma virada de cartão:
Criar os objetos animadores
Para criar a animação de virada de cartão, você precisará de um total de quatro animadores. Serão dois animadores para quando a frente do cartão for animada para fora em direção à esquerda e para dentro a partir da esquerda. Você também precisará de dois animadores para quando a parte de trás do cartão for animada para dentro a partir da direita e para fora em direção à direita.
card_flip_left_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="-180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_left_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="-180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
Criar as visualizações
Cada lado do "cartão" é um layout separado que pode conter qualquer conteúdo desejado, como duas visualizações de texto, duas imagens ou qualquer combinação de visualizações para serem alternadas. Você usará os dois layouts nos fragmentos que serão animados posteriormente. Os seguintes layouts criam um lado de um cartão que mostra texto:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#a6c"
android:padding="16dp"
android:gravity="bottom">
<TextView android:id="@android:id/text1"
style="?android:textAppearanceLarge"
android:textStyle="bold"
android:textColor="#fff"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_title" />
<TextView style="?android:textAppearanceSmall"
android:textAllCaps="true"
android:textColor="#80ffffff"
android:textStyle="bold"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_description" />
</LinearLayout>
E o outro lado do cartão, que mostra uma ImageView
:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/image1"
android:scaleType="centerCrop"
android:contentDescription="@string/description_image_1" />
Criar os fragmentos
Crie classes de fragmento para a frente e para o verso do cartão. Essas classes retornam os layouts que você criou anteriormente no método onCreateView()
de cada fragmento. Você pode criar instâncias desse fragmento na atividade mãe em que quer mostrar o cartão. O exemplo a seguir mostra as classes de fragmentos aninhadas dentro da atividade mãe que as utiliza:
Kotlin
class CardFlipActivity : FragmentActivity() { ... /** * A fragment representing the front of the card. */ class CardFrontFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_front, container, false) } /** * A fragment representing the back of the card. */ class CardBackFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_back, container, false) } }
Java
public class CardFlipActivity extends FragmentActivity { ... /** * A fragment representing the front of the card. */ public class CardFrontFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_front, container, false); } } /** * A fragment representing the back of the card. */ public class CardBackFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_back, container, false); } } }
Animar a virada de cartão
Agora, você precisará exibir os fragmentos dentro de uma atividade mãe.
Para fazer isso, crie o layout para sua atividade. O exemplo a seguir cria um FrameLayout
ao qual você pode adicionar fragmentos no tempo de execução:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
No código de atividade, defina a visualização do conteúdo como o layout que você acabou de criar. Também é uma boa ideia mostrar um fragmento padrão quando a atividade for criada. Assim, a atividade de exemplo a seguir mostra como exibir a frente do cartão por padrão:
Kotlin
class CardFlipActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_activity_card_flip) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .add(R.id.container, CardFrontFragment()) .commit() } } ... }
Java
public class CardFlipActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_activity_card_flip); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() .add(R.id.container, new CardFrontFragment()) .commit(); } } ... }
Agora que você tem a frente do cartão em exibição, é possível mostrar a parte de trás do cartão com a animação de virada de cartão no momento apropriado. Para mostrar o outro lado, crie um método que faça o seguinte:
- Defina as animações personalizadas que você criou anteriormente para as transições de fragmento.
- Substitua o fragmento exibido no momento por um novo fragmento e anime esse evento com as animações personalizadas que você criou.
- Adicione o fragmento exibido anteriormente à pilha de retorno do fragmento. Assim, quando o usuário pressionar o botão Voltar, o cartão será invertido.
Kotlin
class CardFlipActivity : FragmentActivity() { ... private fun flipCard() { if (showingBack) { supportFragmentManager.popBackStack() return } // Flip to the back. showingBack = true // Create and commit a new fragment transaction that adds the fragment for // the back of the card, uses custom animations, and is part of the fragment // manager's back stack. supportFragmentManager.beginTransaction() // Replace the default fragment animations with animator resources // representing rotations when switching to the back of the card, as // well as animator resources representing rotations when flipping // back to the front (e.g. when the system Back button is pressed). .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out ) // Replace any fragments currently in the container view with a // fragment representing the next page (indicated by the // just-incremented currentPage variable). .replace(R.id.container, CardBackFragment()) // Add this transaction to the back stack, allowing users to press // Back to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit() } }
Java
public class CardFlipActivity extends FragmentActivity { ... private void flipCard() { if (showingBack) { getSupportFragmentManager().popBackStack(); return; } // Flip to the back. showingBack = true; // Create and commit a new fragment transaction that adds the fragment for // the back of the card, uses custom animations, and is part of the fragment // manager's back stack. getSupportFragmentManager() .beginTransaction() // Replace the default fragment animations with animator resources // representing rotations when switching to the back of the card, as // well as animator resources representing rotations when flipping // back to the front (e.g. when the system Back button is pressed). .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out) // Replace any fragments currently in the container view with a // fragment representing the next page (indicated by the // just-incremented currentPage variable). .replace(R.id.container, new CardBackFragment()) // Add this transaction to the back stack, allowing users to press // Back to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit(); } }
Criar uma animação de revelação circular
Animações de revelação fornecem ao usuário continuidade visual ao exibir ou ocultar um grupo de elementos da IU. O método ViewAnimationUtils.createCircularReveal()
permite animar um círculo de recorte para revelar ou esconder uma visualização. Essa animação é fornecida na classe ViewAnimationUtils
, disponível para Android 5.0 (API de nível 21) e versões mais recentes.
Veja um exemplo de como revelar uma visualização anteriormente invisível:
Kotlin
// previously invisible view val myView: View = findViewById(R.id.my_view) // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle val cx = myView.width / 2 val cy = myView.height / 2 // get the final radius for the clipping circle val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // create the animator for this view (the start radius is zero) val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius) // make the view visible and start the animation myView.visibility = View.VISIBLE anim.start() } else { // set the view to invisible without a circular reveal animation below Lollipop myView.visibility = View.INVISIBLE }
Java
// previously invisible view View myView = findViewById(R.id.my_view); // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // get the final radius for the clipping circle float finalRadius = (float) Math.hypot(cx, cy); // create the animator for this view (the start radius is zero) Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius); // make the view visible and start the animation myView.setVisibility(View.VISIBLE); anim.start(); } else { // set the view to invisible without a circular reveal animation below Lollipop myView.setVisibility(View.INVISIBLE); }
A animação ViewAnimationUtils.createCircularReveal()
leva cinco parâmetros. O primeiro parâmetro é a visualização que você pretende ocultar ou mostrar na tela. Os dois parâmetros seguintes são as coordenadas x e y para o centro do círculo de recorte. Normalmente, será o centro da visualização, mas você também pode usar o ponto em que o usuário toca para que a animação comece no local selecionado. O quarto parâmetro é o raio inicial do círculo de recorte.
No exemplo acima, o raio inicial é definido como 0, de modo que a visualização a ser exibida fique oculta pelo círculo. O último parâmetro é o raio final do círculo. Ao exibir uma visualização, verifique se o raio final é maior do que a visualização em si para que ela possa ser totalmente revelada antes que a animação termine.
Para ocultar uma visualização que estava visível anteriormente:
Kotlin
// previously visible view val myView: View = findViewById(R.id.my_view) // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle val cx = myView.width / 2 val cy = myView.height / 2 // get the initial radius for the clipping circle val initialRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // create the animation (the final radius is zero) val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f) // make the view invisible when the animation is done anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) myView.visibility = View.INVISIBLE } }) // start the animation anim.start() } else { // set the view to visible without a circular reveal animation below Lollipop myView.visibility = View.VISIBLE }
Java
// previously visible view final View myView = findViewById(R.id.my_view); // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // get the initial radius for the clipping circle float initialRadius = (float) Math.hypot(cx, cy); // create the animation (the final radius is zero) Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f); // make the view invisible when the animation is done anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); myView.setVisibility(View.INVISIBLE); } }); // start the animation anim.start(); } else { // set the view to visible without a circular reveal animation below Lollipop myView.setVisibility(View.VISIBLE); }
Nesse caso, o raio inicial do círculo de recorte é definido para ser tão grande quanto a visualização para que ela fique visível antes do início da animação. O raio final é definido como 0 para que a visualização fique oculta quando a animação terminar. É importante adicionar um listener à animação para que a visibilidade possa ser definida como INVISIBLE
quando a animação for concluída.