Hello there! Some time ago a friend of mine showed me this article (preview below) and asked me if I can do Shared Element Transitions in Flutter. The first thought that came into my mind was “Simple, Heroes!” but when I looked closely, I’ve noticed that the transition in that article occurs in one page, so Hero transitions wouldn’t work in that case. In this post I will use AnimationControllers, Overlays and Rects to get the same result as below:
Setup and GridView
Let’s start with adding images to the project. All we need to do is copy the images into
assets directory. You can find all of them here.
After that, let’s update
pubspec.yaml file, so that Flutter will be able to find those images.
Now all that’s left is creating a
HomePage widget with a
The app should look like this:
Adding a PageView
Now we need to add a PageView. Since we need to keep track of pages, I want the page view to be rendered all the time. On the other hand, I want it to be visible on top of the GridView only when the GridView element was clicked. To achieve such a result, we need to use a combination of
So what is happening now:
- When a user clicks a GridView card, we set
- When a user presses the back button, if the pageView is visible, we set the flag to false.
PageViewis always on top of the
isPageViewVisibleis false, then
PageViewis transparent (
Opacity) and unclickable (
IgnorePointer) so it behaves the same like it wasn’t even there.
Let’s see how it looks:
Syncing PageView and GridView
Right now we can show and hide PageView but it has nothing to do to on what picture we pressed or when we pressed back. We need to have 2 way syncing:
GridViewclick, the corresponding image in
PageViewshould be displayed.
PageViewgets hidden, it should scroll
GridViewto the last displayed image.
First part is simple, all we need to do is add
PageController, which has method
jumpTo. We can use this method to set the page we want, just like this:
From now on, on every click, PageView starts from clicked image:
Now it’s time for the harder part: scrolling to the picture on PageView closed. Similarly to the previous example, we will use
ScrollController which can be attached to the
ScrollControllers don’t have jumpTo(index) method, you can only pass the scroll offset. To overcome this, we need to calculate the offset at which the specific picture is displayed. Let’s take a look at the following method:
Luckily, we know the cards’ height as it is half of the screen width. Having that, we can easily deduct how much we should scroll.
To use that method, all we need to do is add a new
ScrollController to our state class:
The effect? Hiding the PageView causes GridView to scroll:
Getting Rect objects
Now it’s time to work on the transition from GridView image to PageView image and backward. To do that, we need to know the position of both Widgets. We could use
context.findRenderObject method and figure it out, but because I am lazy man, I am going to use DebuggerX‘s awesome rect_getter package from Pub which gives easy access to Wigets’ Rects.
The usage of this package is very simple, we wrap with
RectGetter widget, pass a key and then use the same key to get a Rect object describing the size and position of the wrapped widget. Let’s put it into our code:
What we’ve done here is creating 2 lists of keys for grid images and page images. Each key was passed to specific RectGetter. We also started implementation of the transition method, where we need to access both gridRect and pageRect. Notice that I added
Future.delayed before starting transitions. The problem is that if the transition starts just after
jumpTo methods, it may not get the latest position of images. This solution is more a workaround, if you know a better way to do it, please let me know! 🙂
To create the animation we will need two main components:
Overlays. Let’s start with the animation:
We have our
rectAnimation which will animate from selected grid image to the same page image. Depending on the direction (from grid to page or from page to grid) we decide if we should run
reverse(). But animation itself won’t make any difference if we don’t use it to build Widgets. To do that we will use
Overlay is a handy Widget if we want our Widgets to float on top of others. It has its own Stack, in which we can put
OverlayEntries. Our OverlayEntry will be simple widget wrapped in
AnimatedBuilder so that it is build every time our animation changes. Inside that, we will have an
Image wrapped in
Positioned. We only need to also make sure that the image’s position and size are dependend on current
rectAnimation value. The code looks like this:
The only thing left that keeps us from seeing buetiful shared element transition is using that overlay entry. All we need to do is add it do Overlay when transition is about to start and remove it when it’s finished:
We’ve also added state management to be depended on Animation status, if AnimatedController is completed, we set isPageViewVisible to true, when it starts to go back, we change it to false.
It’s time to see how it all looks like:
The last part is to make the GridView fade into white, while the animation progresses. Luckily, we are already using Stack, so it won’t be a problem:
And that’s it!
I hope you enjoyed this post and from now on the shared element transitions won’t be a problem for you. 🙂
If you have any questions, feel free to leave a comment!
You can see full code for this step here.