UI Challenge – Flight Search
Let’s get to it!
First, we need to decompose this view into a few smaller parts:
- AppBar and top buttons
- Initial input
- Airplane resize and travel
- Dots travel
- Flight stop card view
- Flight stop card animations
- Flight ticket view
- Flight ticket animations
0. Starting point
As a starting point, we need to create a basic Flutter app and ditch all the unnecessary stuff so we end up with only a MaterialApp and a Scaffold:
. . .
1. AppBar and buttons
Stackwidget, which allows us to easily put widgets on top of each other. Now let’s create an
We have created a simple stack containing a
Container, which will be our background as well as transparent `AppBar` which is placed on top of the container. The
height of the container has been extracted because further on the bar’s height changes a little and we would like to be able to reuse the same component. You can also notice a custom
FontFamily. I have downloaded it from here and added it to
pubspec.yaml. I know it is not exactly the same but I’d say it’s close enough 🙂
Expanded. I did only because I didn’t want to do it multiple times when I use the button. If I would actually want to create a reusable widget, I would advise against making it Expanded. Now let’s add those buttons to the HomePage:
_buildButtonsRowinside of a
Columnwhich was inside of a
Positionedwidget is needed because we need to manually put all the content under the AirAsia label but on top of the AppBar background. A
Columnwill be needed later so we can put a card with content under the buttons. At this moment our app looks like this:
. . .
2. Initial input
Stackin which we will place a small
Containerunder the actual
TabBar. You can see it in
The harder part comes with the Input view. It is worth remembering that whenever you are using any
TextFieldsin Flutter you almost always want to wrap them inside some sort of scrollable views like
ListView, so that when the keyboard appears your layout won’t get disrupted. However, in our example, we also want to place a
FloatingActionButtonat the bottom of the view. Technically we could place it under the
ScrollViewbut that would cause it to be always there even if there are more fields to be scrolled to. We could also place it just inside the
ScrollView, but then it wouldn’t be aligned to the bottom if there was a space for it.
The solution to that problem is using the following combination of widgets:
LayoutBuilder which will provide us access to
BoxContstraints, then we use
ConstrainedBox and we pass maximum height we want our layout to have (obtained from constraints). In the end, we need
IntrinsicHeight so that our view will fill as much space as it can but it will be able to render it in `ScrollView`. Now we can have a Fab that is aligned to the bottom but will also be scrollable if more content occurs.
Now let’s update home_page‘s line:
Container(), //TODO: Implement a card
. . .
3. Plane resize and travel
TabBarnames remain the same. Then there is a short delay after which plane starts to go forward with a simultaneous change of names in tabs (I will ignore that small animation of the names).
Let’s start with resizing animation. First, we will create a new Widget called PriceTab (to be honest I am not sure why it is a price 😀 ). We will place everything inside a
Stack and we will mostly operate on
Positioned widget to put widgets where we want them. In general, it is not trivial to get Widget’s size before it gets rendered but luckily we have wrapped our tab inside of
LayoutBuilder so we have access to view constraints and therefore to Widget’s height. This way we will be able to calculate the widget’s padding top as followed:
PriceTabwidget to the
Animationobjects related to the size of the plane.
_planeSizeAnimationControllerwhich is a parent of actual size animation
_planeSizeAnimation. This animation will scale from 60 to 36 which is the actual size we would like to achieve. We pass it to the
AnimatedPlaneIconwidget which will be rebuilt on every animation change.
- We have created a new Animation Controller.
- We have added an Animation to that controller, to get access to nice curve instead of a linear one.
- We have changed
_planeTopPaddinggetter so that it is dependent on
- We have added an
AnimatedBuilderwhich will rebuild the plane icon according to Animation’s value.
The last thing for that part is to add a trail and change the names in the tabs. When it comes to the trail, we just need to add it to
_buildPlane() method. For now, let’s keep the length of it as 240.0.
EDIT: There was missing
- _minPlanePaddingTopin the
_maxPlaneTopPadding. Add it to move the plane a bit from the bottom edge.
. . .
4. Dots travel
We can start by creating a dot class
_dotsAnimationControllerwhich will control all dots animations. The interesting part is how every dot is animating. Technically we could create 4 animation controllers each to every dot but there is a better solution to that. We can use an
Interval is an animation curve, that allows us to specify at which moment of whole animation (defined by AnimationController) we want a single animation to start. For example, if we specified in the controller the duration to 10 seconds and we created animation with an interval with start equal to 0.3 and end equal to 0.7 then our animation will start with 3-second delay and end after 4 seconds. This way we can have multiple animations overlapping with each other controller by one controller. We defined our animation to take 0.4 of time declared in a controller and each animation starts 0.2 after the previous one. We also defined what is the original and final position to every dot based on its index and card height. We have added all animations to the list so that we can pass them to
AnimatedDot widget which will handle drawing a dot.
. . .
5. Flight stop card view
isLeftflag. Since the card can be placed on both sides, it will be built differently so we want to pass that information to it. The widget has specified
widthfields which are describing actual card’s dimensions. However, it is hard to say how big the actual widget will be. To get the maximum width of the widget, we are using render box’s constraints. Even though this approach might seem easy, it cannot be always used because during the first build, the framework doesn’t actually know those constraints yet. Luckily we will animate this view and on the first build it will have zero size, so we can get out with this.
Last thing worth mentioning are those
buildMargin methods. All the texts in the card are either aligned top, right, bottom, left or center of the card depending on the text and if the widget
isLeft. Those methods will be developed in the next section. For now, it’s important that they work 🙂
Let’s see how to place those cards inside our app:
Expandedwidget on the left or on the right of the card. Having the card also wrapped inside the
Expandedwe can make sure that it will take only half of the width.
. . .
6. Flight Stop Card animations
getMarginmethods so that they will be dependent on animation’s value.
ElasticOutCurvewhich is the actual curve of texts’ animations. We do it to decrease the bouncing effect. I won’t describe how the getMargin methods changed, they just work :). Each text widget is now passing animationValue to those methods as well as to its own font size. We have also added a public method
runAnimationso that parent can decide when the card should be animated.
onInit()method we create
GlobalKeyswhich we later pass to
FlightStopCardsso that we have access to the state and we can start the animations. We also added a
FloatingActionButtonso that whole view will be completed. This fab’s animation will start just after last card animation’s start. This time, we use
Future.forEachto run a delayed animation start for each card. This is how it looks like:
. . .
7. Flight ticket view
materialwith a very light shadow and then I clipped it with a smaller radius (If you have a better idea, please share! 🙂 ). This is what we got:
. . .
8. Flight ticket animations
Intervals. I guess that at this point we don’t have to elaborate on that. If there is anything unclear, leave a question :).
. . .
And that’s it folks!
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.
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!
Check out other posts!
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!
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!