There is a good practice to make your application beautiful and live, and nowadays there are a lot of tools and ways to achieve this. One of them is Shared Element Transition.
In this article I’ll cover a few mistakes which have cost me a lot of time; I’ll show how to avoid them if you decide to implement this kind of transitions with Fragments on application.
Get started
Before making the animation I’ve read dozens of articles but most of them were about Activity Transition. However, I came across a really good ones about Fragments and I want to give a little recap on how to create Shared Element Transition.
Here are main steps to create animation:
- Enable
setReorderingAllowed(true)
. It allows to reorder a calling the lifecycle methods and your animation will be displayed correctly. - Call
addSharedElement()
and add the views that will be shared between screens - Add unique
android:transitionName
on each view in transition - Add
sharedElementEnterTransition/sharedElementReturnTransition
in destination fragment. Optionally: for better effect we can also setenterTransition/exitTransition
. - Add
postponeEnterTransition/startPostponedEnterTransition
to define the moment when the data is loaded and UI is ready to be drawn
Seems like that’s enough to build animation and make your designers and users happy. BUT there are always some accidents. Let’s take a look what we’ll have if we take the steps listed above:
That’s not what we expected. Let’s figure it out.
Mistake #1. Static transitionNames (half a day wasted)
As I said before, our Views should have unique transition names — otherwise the transition framework won’t be able to recognize which View take part with transition and will make it without them. So where is a problem?
The thing is RecyclerView. That’s it.
If there is RecyclerView and an animating view is a part of RecyclerView item, you should build and set transitionName
dynamically (forget about XML). Besides, you should do it in both fragments even if the second one doesn’t have RecyclerView.
So the fix is:
val transitionNameImage = context.getString(R.string.transition_image, title)
You might have noticed I put «title» as an argument to get a unique name. It’s better to use domain model instead of, for instance, item position on the screen, because there is no need to pass these names as arguments to Bundle and parse them from the second Fragment.
Mistake #2. Not considering parent fragment (1.5 days wasted)
I know, you might ask «How come?». But when I was reading the articles about shared animation, no one considered an example in complex fragment’s hierarchy. That’s why you might sometimes not pay attention to it.
So, my first fragment was a child of fragment’s container and no wonder that postponeEnterTransition()/startPostponedEnterTransition()
had no effect.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
parentFragment?.also { parentFragment ->
NewsTransitioner.setupFirstFragment(parentFragment)
parentFragment.postponeEnterTransition()
}
// Initialization UI
}
What you need to do here is to call these methods from parent fragment.
Mistake #3. Underestimating Glide (2 days wasted)
«Ok, I’ve learnt how to make shared transition, when to call required methods regarding the lifecycle and the loading. This time it’s gonna work!»
Well I was wrong. This is perhaps the trickiest mistake I’ve faced. Let’s take a look at what we have so far:
You may notice there is a weird glitch with enter transition. When it starts, the image has already changed matrix and then just move to the final position.
I don’t want to describe the whole investigation here. Long story short, I was lucky to stumble across a nice article.
Where I found solution. Here it is:
“We have this glitch because Glide tries to optimize image loading. By default, Glide is resizing and trimming images to match the target view.”
In order to fix it, I added, no jokes, a single line of code like this to initialization Glide’s chain:
Glide
.with(target)
.load(url)
.apply(
RequestOptions().dontTransform() // this line
)
So, you should disable any Glide’s transformations on images if they’re involved in a shared transition.
Mistake #4. Incorrectly managing postPostponeTransition()
Honestly, it’s not exactly a mistake but still I assume it would be good to mention.
When it comes to manage postPostponeTransition()
and startPostponedEnterTransition()
methods, you should select the right moment. The moment is when the UI is already to be drawn.
There are two main points we should know before calling the methods:
- on the one hand, when images with transition are loaded and ready
- on the other hand, the views hierarchy are measured and laid out
For images usually we use Glide and it has a fancy listener:
RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
startPostponedEnterTransition()
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
startPostponedEnterTransition()
return false
}
}
Note that we call startPostponedEnterTransition()
in both cases: success and error. It’s vital because if we don’t make it your application will freeze.
In cases when transition comes without any images you may use doOnPreDraw()
extension method from ktx on the root layout and do startPostponedEnterTransition()
over there.
BONUS: Speaking of RecyclerView, it’s not enough to simply add a listener for images. We should retain an item position of RecyclerView where transition starts from. When the user goes back to the previous screen, we should compare image loaded position with the retained position at the listener and start transition only when they are matched.
Putting all together
In this article, I’ve showed some gotchas you might face implementing a shared transition with fragments and the ways to deal with them.
Briefly, here they are:
- Keep in mind fragments hierarchy (don’t forget about parent fragment)
- In case with RecyclerView, always build transition names dynamically (source + destination)
- Disable any Glide transformations
- Do calls
postPostponeTransition()
andstartPostponedEnterTransition()
correctly regarding your logic.
Thank you for reading and see you next time!
P.S. If you wonder how to animate image corners, here the code, just add to the rest shared animation transitions.
fragment.sharedElementEnterTransition = TransitionSet().apply {
addTransition(ChangeImageTransform())
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeOutlineRadiusTransition(animationRadiusData.startRadius,
animationRadiusData.endRadius)
)
}
Автор: rcmkt