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 a 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!
The 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:
. . .
Icon 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 the 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. ?
PLACEHOLDER
The design
The implementation
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!
Join the newsletter!
Join the newsletter to keep track with latest posts and get my special Widget to animate views' entrances without any hassle for FREE!
Marcin Szałek
Founder of Fidev
Flutter enthusiast since Alpha release in 2017. Believes that sharing is caring, which lead him to start a technical blog dedicated fo Flutter in its early days. Loves to see beautiful designs become real apps and is willing to help make it happen. Enjoys sunny beaches far from home.
Check out other posts!
Stripe Checkout in Flutter Web
Flutter Web is getting more mature every day. If you want to accept payments using Stripe Checkout in your Flutter web application, this article is just for you!
Stripe Checkout in mobile Flutter app
Have you ever struggled to integrate card payments into your mobile Flutter app? If so, today is your lucky day! In this post, I present how to use Stripe Checkout in the Flutter app without any hassle!
Interacting with Widgets using Framy
Have you ever developed a widget or a page and you wanted to make sure it works correctly in different scenarios but then it turned out that you can’t just reproduce all the cases you want to cover? Framy may solve such problems!
You are super man in Flutter. Maybe we republish your articles in our website.
Mail me on marcin@fidev.io 😉
sure
So good, thanks very much for sharing!
You’re welcome!
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,
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.