Drag and Drop with Morphing Animation in Flutter

Pierre Martin
5 min readMay 28, 2024

Flutter offers a lot of power, but it also has its complexities and subtle details. Last week, I took on a Flutter UI challenge. The project was super fun to build but it turned out to be surprisingly tricky. I’ll break down the project and explain the steps I took to create this interaction.

Interaction Overview

Let’s start by taking a closer look at the interaction itself. There are many things happening simultaneously. Here’s the breakdown:

  1. First, the user drags a card from the horizontal item bank.
  2. When the drag starts, the blank spaces between items gently inflate.
  3. As the user drags the card over the vertical list, the items already present move down to create an empty space in anticipation of the drop.
  4. Upon dropping, the card being held is animated towards its new position in the vertical list, changing its shape along the way to match the style and shape of the vertical cards.
  5. Finally, spaces in both the horizontal and vertical lists deflate, returning to their normal state.

Inflating Lists

There’s nothing overly complex here; the inflation animation is simply created using an Animation that drives the width of SizedBox widgets between cards.

However, removing the item card from the horizontal list proved to be more challenging. When an item is removed from the horizontal list at the top of the screen, it causes the remaining items to ‘teleport’ to their new positions without animation, which feels janky.

My initial approach was to use an AnimatedSize widget to smooth out the width change. However, when the user removes an item from the middle of the list, it doesn’t work anymore because the AnimatedSize widget animates its own size and aligns the content inside it. It works well when the user removes the item at the beginning or the end of the list, but not when an item is removed from the middle.

I came up with a simple yet effective solution that simply consists of saving the removed item’s index in the widget’s state and using it to insert another animated white space in place of the old item. The width of this “placeholder” matches the original card width and is animated toward zero. It’s driven by a second animation triggered when the drag-and-drop interaction is validated.

The Morph Animation

The most critical aspect of this interaction, the morphing effect, has also been the most challenging.

My initial attempt involved animating the horizontal card to move from the position of the dropped card toward its final place in the vertical list. However, I quickly discovered that being constrained by the card’s layout, composed of regular columns and rows, caused two annoying issues.

First, I had to transform the animated element from a vertical to a horizontal layout, but there is no such widget as AnimatedFlex that could handle this problem out of the box.

Second, this approach created z-index issues where, in some conditions, the animated item could be drawn below other items in the list. This happens because Columns and Rows draw their children in their definition order, no matter how they are transformed.

Overlay to the Rescue

Hopefully, Flutter has an overlay system that lets you draw widgets on top of everything else. This is ideal in this case since it will tackle the z-index problem.

For the layout issue, instead of trying to animate the horizontal card from the drop position as an entry animation, I have decided to create a third widget responsible for the morphing animation, displayed in the overlay layer. In fact, this is exactly how Hero widgets work under the hood.

It’s worth noting that Hero can’t be used directly in this case since the animation takes place on the same screen; Flutter’s Hero system only works when pushing new routes.

Hero Flight

To create a third widget responsible for displaying the flight animation, I needed to know the position and size of every widget that is part of the morphing animation and their final destination. Once you have that, it’s just a matter of animations and tweens.

I believe the true power of Flutter resides in the fact that you can access low-level rendering APIs. In fact, you can even create your own custom rendering algorithm that differs from the traditional Flutter layout system.

Thanks to this low-level access, I’ve been able to access both the dragged item and the targeted horizontal card using GlobalKeys.

However, to access the position of the targeted horizontal card, it needs to be rendered first. This is annoying since we don’t want it to be displayed before the morphing animation ends. Fortunately, we can take advantage of the Visibility widget, which lets us lay out the destination widget without displaying it.

After I got all the start and end positions and sizes, I can just put everything in a Stack and use a Transform widget to animate widgets all the way through for the morphing effect. I could have used Positioned here, but since I’m working mostly with Offsets at this point, it’s just easier to take advantage of the Transform.translate constructor, which takes an Offset as a parameter.

This setup is quite convenient because it lets you get rid of any layout constraints, and you can define the rendering order you like. That way, widgets won’t interfere with each other. Even if it’s a bit cumbersome to set up with all the GlobalKeys, the possibilities are endless and you can do virtually anything.

Going Further

That’s pretty much it for this project! In total, it took around 2000 lines of code and 30 hours of work to get a final version.

However, now that I’ve found the Overlay + Stack pattern, I think I would be able to reproduce this kind of animation in a few hours. Doing code challenges is a great way to push you out of your comfort zone and learn new skills, plus it’s always super fun to do!

If you want to go deeper into the implementation details, the full source code of the project is available on GitHub.

Thanks for reading and happy Fluttering!

--

--