5.11. Animations and Transitions
There are two types of animations: animations at the view-level or object-level; and propertly-level animations.
View-level or object-level animations
This type of animation is not resource-intensive from the CPU point of view. However, only a few properties of views can be animated in this way. Using this method is feasible only if the views we animate only have a visual purpose and we have no constraints related to their position in other elements of the layout.
If our views require user interaction or we have position constraints related to other views, serious problems may occur. For example, if we animate the position and have a click
listener, the listener continues to observe the previous position. Another anomalous situation occurs when the position of the view is restricted in relation to other items. When you animate a view with these relative constraints, it will stop fulfilling them.
Taking into account these limitations, let us take a look at some examples:
First, we create a resource folder of type anim
.
In that folder we create an “Animation Resource Type” with root set
. Within the file we create a translation and animation of the alpha
.
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<translate
android:fromXDelta="0.0"
android:toXDelta="200.0"
android:duration="2000" />
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="2000"
/>
</set>
To invoke it from our Kotlin code we use the following:
val animation1: Animation =
AnimationUtils.loadAnimation(applicationContext,com.uoc.layouts3.R.anim.animation3)
binding.image1?.startAnimation(animation1);
where animation3
is the name of the resource we have created within anim
; and image1
is the view on which we want to apply this animation.
Moreover, we can also create an animation directly in Kotlin. For instance, here we create a set consisting of an alpha
animation and a translation animation:
val set = AnimationSet(true)
val alpha = AlphaAnimation(0.0f, 1.0f)
trans val = TranslateAnimation(0f,200f,0f,0f)
set.addAnimation(trans)
set.addAnimation(alpha)
set.duration = 3000
set.fillAfter = true
binding.image1?.startAnimation(set)
Property-level animation
This type of. animation does respect the constraints and events assigned to the object, but on the other hand it is less smooth.
In the following example, we can see how we animate the marginLeft of an image that has a listener of a click event and that in turn has restrictions with another button type component.
val animator = ValueAnimator.ofInt(10, 200)
animator.addUpdateListener { animation ->
val cons:ConstraintLayout.LayoutParams = binding.image1?.layoutParams as ConstraintLayout.LayoutParams
cons.leftMargin = animation.animatedValue as Int
binding.image1?.layoutParams = cons
}
animator.duration = 1000
animator.start()
Property-level animation.
Source: Javier Salvador (Original image) License: CC BY-NC-ND 4.0
If we want to simulate that our view appears from one side of the screen, we simply position it initially off the screen, android:layout_marginLeft="-200dp"
, and the animation will move it inside.
Learn more: You cannot directly execute animations in the
onCreate
callback of an Activity. If we want to do this, we must postpone the execution of the animation with a timer or a deferred call to a handler.
Transitions
We can create custom input and ouput transitions for out fragments. They are assigned in the onCreate
callback of the fragment.
For example, we can define the transition called transition1
in the transition
directory.
<slide xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:slideEdge="left" />
In the onCreate
callback of the fragment, we define that this will be our input transition:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
val enterTransition1 = inflater.inflateTransition(R.transition.transition1)
enterTransition = enterTransition1
}
Transitions between fragments sharing items
These are one of the most spectacular transformations. The idea is that an element of a view of the first fragment moves from its position and size to the position and size of another element in the second fragment. All this is done while the rest of the elements in the first fragment disappear and the rest of the elements in the second fragment appear.
Let us consider a typical example: tapping on one item of a list allows us to view its details. You can see a video of the result we want to achieve here.
In the layout of the item in the list we define which element we want to move. Then, we assign a transitionName
to it.
<ImageView
android:id="@+id/item_image"
android:layout_width="match_parent"
android:layout_height="300dp"
android:scaleType="centerCrop"
android:background="#dddddd"
android:transitionName="itemImage"
/>
In the layout of the “details” fragment, we define the view where the item will end up being, also declaring the same transitionName
.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.detail.DetailFragment">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/item_image_detail"
android:layout_width="match_parent"
android:layout_height="500dp"
android:scaleType="centerCrop"
android:background="#dddddd"
android:transitionName="itemImage"
/>
<TextView
android:padding="20dp"
android:id="@+id/item_text_detail"
android:text=""
android:textSize="20dp"
android:textColor="#000000"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</FrameLayout>
When you tap the list item, the adapter of the view recyclerView will run the callback onClick
. In our case, the callback has been passed to the method adapterOnClick
defined in the Fragment containing the recyclerView.
private fun adapterOnClick(item: Item) {
(activity as MainActivity?)!!.openDetail(item)
}
As you can see, the Fragment invokes the method openDetail
from the MainActivity. Inside openDetail
, we first create the detail fragment and assign the item to it.
var f3:DetailFragment? =
nullf3 = DetailFragment.newInstance("", "")
f3.item = item
Then, we state the type of input and output transition of the element shared by the new fragment:
f3.setSharedElementEnterTransition(DetailsTransition())
f3.setSharedElementReturnTransition(DetailsTransition())
This class DetailsTransition
is defined as folows:
setOrdering(ORDERING_TOGETHER)
: Applies all transformations at once. If we useORDERING_SEQUENTIAL
instead, then transitions will be applied one after the other.ChangeBounds:
Allows changing the position, width and height of the view.ChangeTransform:
Allows rotation and scaling transformations if needed.ChangeImageTransform:
Allows for image resizing.
class DetailsTransition : TransitionSet() {
init {
setOrdering(ORDERING_TOGETHER)
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
}
}
In the supportFragmentManager we add the addSharedElement
passing the source view as the first parameter and the transitionName
as the second parameter.
supportFragmentManager.commit {
addSharedElement( item.view!! , "itemImage")
replace(R.id.placeholder1, f3!!)
addToBackStack(null)
}
If the input element belongs to a recyclerView item, we have a problem with the output transition: it cannot be performed because the views representing the items have been destroyed and are not available at the time of the output animation. The best solution if the recyclerView is part of a fragment is not to use remove
or replace
with the fragment containing the recyclerView: we will simply hide it and add the “details” view. In this way, the views wil be available when the user presses the Back
button.
In this example, f2 is the fragment containing the recyclerView and f3 is the fragment representing the “details” view.
supportFragmentManager.commit {
addSharedElement( item.view!! , "itemImage")
hide(f2!!)
add(R.id.placeholder1, f3!!)
addToBackStack(null)
}