Material Transitions in Flutter with the Animations Package

Sep 21 2021 · Dart 2.13, Flutter, VS Code 1.59

Part 1: Material Transitions in Flutter with the Animations Package

02. Understand the Container Transform Animation

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 01. Get Introduced to the Material Motion System Next episode: 03. Create a FadeThrough Animation

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 02. Understand the Container Transform Animation

Let’s dive deeper into the OpenContainer widget by using it with a custom widget. For this episode, we’ll be animating the card from the home to the detail page. Currently, if we click the card, it navigates to the detail page using the platform specific route animation. Let’s change that up to a container transform animation.

Open up the home_page.dart file. Scroll down to the article card widget. Currently, the ArticleCard is wrapped with a GestureDetector widget. And when tapped, it navigates to the article detail page. Let’s update our code to use the OpenContainer widget like so:

OpenContainer(
    closedBuilder: (BuildContext _, VoidCallback openContainer) {
        return ArticleCard(
            article: article,
        );
    },
    openBuilder: (BuildContext _, CloseContainerActionCallback closeContainer) {
        return ArticlePage(
            article: article,
        );
    },
),

Save your work after the update. Now, i noticed something. The container does not transform from the bounds of the card. Instead, the transformation starts from the outer box around the card. And this is the OpenContainer widget.

This is something similar with the FAB example from the last episode. Remember, the OpenContainer widget just wraps around the widget returned from the closedBuilder. From the look of things, it seems that the ArticleCard widget has some spacing around it.

Let’s head over to the ArticleCard widget. I’ll hold down Ctrl and click on it to open it up. You can also press the F12 key if you’re on mac or windows machine to open up the definition. And we can see the culprit already. The Card has a margin of 16. Let’s remove that. Then save your work.

Opps!!! We’ve lost the spacing but dont worry about that now. Let’s try it out to see if it transforms as expected. Nice, the OpenContainer transforms between states from the correct bounds as expected.

Now, let’s add back the margin. We wont be adding back the margin in the card widget obviously. Go ahead and remove the Card widget entirely. Next, head back to the home_page.dart file. I’ll add a padding of of 16 to the ListView:

padding: const EdgeInsets.all(16),

The ListView now has the required padding for its children. Next, we need to add some spacing between the list items. To do that, i’ll wrap the OpenContainer with a Column widget. Then add a SizedBox with a height of 16 below the OpenContainer.

const SizedBox(height: 16),

Save your work. We now have the spacing between the items.

Note: The ArticleCard still has a border radius even atfer removing the Card widget in the article_card class. The border radius is actually coming from the OpenContainer widget becasue it has a closedShape property. This property sets the border radius to a default valur of 4 which is the same as the default border radius of a card.

Finally, let’s add a drop shadow for the closed container. The closedElevation of the OpenContainer is set to 1 by default, let’s go ahead and change it to 4 to match our initial card design.

closedElevation: 4,

Save your work. Now, the UI is back to its original look. Let’s try out our animation once more. Cool, everything works fine and our container rightly animates from the bounds of the closed container to the open container.

As you can see, the main idea behind creating nice transitions is just to make sure the OpenContainer matches the bounds and styling of the widget the closedBuilder returns.

Next, the default transition between the contents of the incoming element and the outgoing element is a fade transition. This simply fades the incoming element over the outgoing element. But sometimes, the incoming element might have some transparent parts and using the default fade could make the animation seem like it has some overlapping frames. We could use a fadeThrough transition to minimize the appearance of overlapping frames.

Let add that now. Update your code to the following:

transitionType: ContainerTransitionType.fadeThrough,

The fadeThrough transition, first fades outs the outgoing element and then fades in the incoming element. With this approach the incoming and outgoing elements cant overlap each other during the transition. Save your work and try it out.

If you notice, after the outgoing element fades out, our card becomes blank and then the incoming element fades in. Next, let’s talk about the arguments of the closed and open builders. The second argument of the closedBuilder is a callback function that we can call to programmatically open the container. By default, the whole child of the OpenContainer is tappable.

But sometimes, we might not want this behaviour. We might want only a certain portion of the child to be tappable or we might want to do something else before triggering the container to open.

To see this in action, update your code to the following:

...
tappable: false,
closedBuilder: (BuildContext _, VoidCallback openContainer) {
    return GestureDetector(
    onTap: () {
        openContainer();
        print("Container Opening...Do Something here...");
    },
    child: ArticleCard(article: article),
    );
},

First, we set the tappable property of the OpenContainer to false. This prevents the entire container to fire the open action. Next, we wrap the ArticleCard with a GestureDetector and inside the onTap method, we call the openContainer() callback to trigger the open action. And as you can see, this gives us more control if we want to do other stuffs while opening the container.

Let’s try it out. And everything works as expected.

The same approach could be used for triggering the close action inside the openBuilder but the closeContainer action is rarely used because the Navigator.pop() method triggers the close action by default. This is possible because the OpenContainer widget uses a PageRoute behind the scene.

When the container is tapped, this PageRoute is pushed on top the closest Navigator stack. And that’s why we could reverse the animation whenever we click the back button in the appbar because we’re simply poping off the PageRoute from the Navigator stack.

Speaking of Navigator.pop(), whenever we pop a route and the container is returned to the closed state, the onClosed callback function is called. Let’s implement it:

onClosed: (result) {
    print("Container closed");
    print("We can reload the list of articles here");
    print(result);
},

We could do stuffs like refetching the data or if the popped route returned any data, we could use it in here to do whatever we want. Note: for the result to be returned, in the onClosed callback, make sure you explicitly add the return type for the openBuilder’s action.

Okay, let’s try it out. First, i’ll make sure the debug console is open, if not then press Ctrl + Shift + Y or Cmd + Shift + Y if you’re on a mac to open it up. I’ll go ahead and click to open the container. And then i’ll close it.

If you look at the debug console, we can see that the print statements are displayed there when the container was closed. And notice, the result returned from popped page is null and that’s simply because we didnt return any data from that page.

To return some data, head over to the article_page.dart file. I’ll add the leading property to the SliverAppBar to override the default back button by pasting the following code:

leading: IconButton(
    onPressed: () {
        // Navigator.pop(context); // Default pop() implementation
        Navigator.pop(context, "Data from the Article Page");
    },
    icon: const Icon(Icons.arrow_back),
),

The pop method has an optional result argument where we can pass data. In this case we just simply pass a string but we could pass more useful data like the id of the aritcle we just viewed. It all depends on your use case.

I’ll save my work and try it out once more. And you can see the string is passed as the result of the onClosed callback function.