Advanced transitions – UI Tickets Challenge

Advanced transitions – UI Tickets Challenge

As developers, we have always get into situations where the designers require some fancy transitions which look awesome on the design but are extremely difficult to implement. Being Flutter developer makes it different, makes it fun because we have tools to do it! To prove it in this post, I will continue implementing awesome Buy Tickets design by Dldp and add bottom sheet transitions! Let’s do it!

 

Original design

Starting point

In the previous post, we have created a parallax effect for the cards and created a simple bottom sheet. We will leave the page content untouched, but we will need to remove the bottom sheet and write from scratch. What we need is to have a Stack widget in order to have full control over how the ExhibitionBottomSheet will be placed.

And let’s have a simple blue sheet so far:

The Positioned ensures that the element sheet is always expanded horizontally, it is aligned to bottom and has a proper height (which will be changed later on). The Container so far specifies only padding and decoration, which gives us nice rounded corners on top and accurate background color.

Starting point

Expanding the sheet

The sheet’s height will be defined by AnimationController. We will wrap the whole widget in AnimatedBuilder, because almost every widget in that view will depend on the controller’s value and will need to be rebuilt whenever that value changes. To expand the sheet, we can use AnimatedController.fling method, which will basically snap the sheet in a given direction. We can call this method by wrapping the sheet inside the GestureDetector and providing onTap callback:

You can also notice we have added a lerp function. This function is just a helper to interpolate a double value between its min and max value based on the controller’s progress. We could probably use Tween instead as it does a similar thing but I find lerp function suiting well here. We can see how the tap causes the sheet transitions between small and big one:


Dragging the sheet

At this moment the user can tap to open the sheet but we also need to provide an option to hold the sheet and manually move it to the top. There are two things we have to keep in mind. The first one is to update the sheet’s height alongside drag updates. The second one is to finish the animation once the user has finished the gesture (he or she can just snap and expect the sheet to get expanded).

Luckily for us, this behavior was implemented in Google’s Gallery App which is open sourced, so we can borrow the gesture handling from the Google team and use it in our app. 🙂

Now when the user drags the sheet, we can update controller’s value, also when the user ends the drag gesture, we can check it’s velocity and decide whether we want to complete the animation or dismiss it.


Icon and title transitions

I think it’s time to put some content inside our Container. Let’s start with the icon as it’s the easiest one. Since we will need control of every Widget’s position, we will place them inside the Stack. This way we can wrap every Widget in Positioned and specify its position. For the MenuButton it will look like this:

Now let’s add the SheetHeader. The widget itself will require a fontSize and topMargin in the constructor, as those parameters will change alongside the animation’s state.

To pass them, we will create new getters which will depend on the animationController’s value. We are going to need headerTopMargin and headerFontSize:

What it means is that for example headerFontSize starts with the value of 14 but the bigger the sheet is, the bigger the font size gets finishing at 24 on a fully expanded sheet.

Now we can see how the icon is placed in the same position but header changes based on transition’s progress:


Icons transitions

It’s time for the main attractions, which are the icons. First, we need to create a model and list of events which will be displayed in the sheet.

Now let’s think about what we need to do to change the icons from horizontally oriented small ones to vertical big ones. Since we are already using Stack and Positioned we can just adapt icons left and top alignment based on the controller’s value (similarly to top margin in the title). The only tricky part is to remember, that we need to take event’s index into account so that every next icon is placed more into the right (horizontally) or more into the bottom (vertically). We can also modify the icon’s size and corner rounding. The getter methods should look like this:

It’s that simple! Knowing how much space should widgets take, we can calculate the exact position we want them in. And knowing the exact position of the widget, we can pass leftMargin and topMargin to Positioned to get the effect we wanted:


Ticket details

The last part is to add the ticket details once the animation is completed. Since we know the position of icons, we can use it also for the expanded items – this will be easy. Theoretically, we could just decide, that if _controller.status == AnimationStatus.completed, then show the details, otherwise don’t do it. Such an approach would make us lose the opportunity to add some nice fade animations in that process. Instead, we will use AnimatedOpacity so that the expanded item will nicely fade in and fade out when it’s supposed to be displayed or hidden. This is my approach, not sure if it’s the best but at least it’s simple, it is possible that linking the item’s opacity with the controller’s value can provide a better effect. As always, it’s up to you. 🙂

The widget is quite long, but it’s mostly just organizing and displaying data – nothing exciting. Depending on what we pass in isVisible parameter, it will cause widget transitions between visible and invisible. If you dive in closely, you can also notice that ExpandedEventItem has margin-left equal to the height. It’s because this widget is meant to be placed under the icon widget. This way we make space for the icon to provide an illusion that it’s a part of the details card.

Now we need to add those details to the main stack before the icons, so they will be rendered under them. We will also modify rounding corners to match the designs.


And that’s it!

Now before we compare the results, let me point out that in this post I focused only on implementing the effects from the design. If you would like to use it in your own app, I would advise adding limits on how many items you display in horizontal view (as they would go over the menu button). Also, I would replace the whole view with the ListView when the sheet is fully open. Right now those elements are not scrollable but I guess they should be. 🙂

 

Final result
The original design

I hope you enjoyed going through the process with me and that you feel ready to implement it in your app as well. 🙂

If you find this post helpful, please share it with others, so they can learn too!

You can find the full code on the GitHub repository (feel free to leave a Star if you like it).

Cheers 🙂

Be sure to check out the previous post where I implemented parallax cards from the same design and other UI Challenges on my blog in here!

7 thoughts on “Advanced transitions – UI Tickets Challenge

  1. Hey Marcin, thank you so much for this article, If I have a list of events (more than three events) how can I add a a scrollableView, because I want to show other content bellow the liste, I tried to add SingleChildScrollView like bellow (not working :/):

    SingleChildScrollView(
    child: ConstrainedBox(
    constraints:
    BoxConstraints(minHeight: 200.0, maxHeight: 400.0),
    child: Container(
    child: Stack(
    children: [
    for (Event event in events) _buildFullItem(event),
    for (Event event in events) _buildIcon(event),
    ],
    ),
    ),
    ),
    ),

    Thank you so much for your help,

    1. As I mentioned in the ending of the post, you should replace the view with the ListView once the animation completes. Wrapping those items in SingleChildScrollview can’t work as they are using Positioned which is meant for Stack, not Column.

Leave a Reply

Your email address will not be published.