Shared Element Transition in Flutter

Shared Element Transition in Flutter

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:

Expected result
Expected result

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 GridView:

The app should look like this:

Initial GridView

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 Stack, Opacity and IgnorePointer widgets.

So what is happening now:

  • When a user clicks a GridView card, we set isPageViewVisible to true.
  • When a user presses the back button, if the pageView is visible, we set the flag to false.
  • PageView is always on top of the GridView.
  • If isPageViewVisible is false, then PageView is 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:

  • On GridView click, the corresponding image in PageView should be displayed.
  • When PageView gets hidden, it should scroll GridView to 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 GridView. Unfortunately, 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:

 Transition

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! 🙂

Animated image

To create the animation we will need two main components: Animations and Overlays. Let’s start with the animation:

We have our AnimationController and 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 forward() or 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.

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:

Animated curtain

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.

Cheers 🙂

 

Leave a Reply

Your email address will not be published.