UI Challenge – Flight Search

by Jul 31, 2018Flutter26 comments

In this post, I will go through another UI Challenge. I have picked Johny Vino‘s Flight Search design from 100 Mobile App Interactions which is mostly about animations and I will try to implement it as close as I can.

Let’s get to it!

First, we need to decompose this view into a few smaller parts:

  1. AppBar and top buttons
  2. Initial input
  3. Airplane resize and travel
  4. Dots travel
  5. Flight stop card view
  6. Flight stop card animations
  7. Flight ticket view
  8. 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

Since our AppBar is a bit expanded and all the views are going to be written on top of it, all the views in the application will be based on a Stack widget, which allows us to easily put widgets on top of each other. Now let’s create an AirAsiaBar widget.

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 🙂

The last thing is to add the bar to the HomePage:
This is how the app bar looks for now:
Now let’s add the 3 buttons on top. Since there are not the buttons we are used to using, I will create my own one:
I don’t think there is anything worth explaining here, maybe except the fact that the button is wrapped in 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:
As you can see, I placed _buildButtonsRow inside of a Column which was inside of a Positioned. The Positioned widget is needed because we need to manually put all the content under the AirAsia label but on top of the AppBar background. A Column will be needed later so we can put a card with content under the buttons. At this moment our app looks like this:
You can see the full code for this step here.

. . .

2. Initial input

Now, we need to create a card that will contain most of our views. Even though it might seem pretty straightforward there are some problems that need to be solved during this part but first let’s see the code:
You can notice that in the design there is a gray line behind the tab indicator. To achieve this effect, we have to use a Stack in which we will place a small Container under the actual TabBar. You can see it in _buildTabBar() method.
The harder part comes with the Input view. It is worth remembering that whenever you are using any TextFields in Flutter you almost always want to wrap them inside some sort of scrollable views like CustomScrollView or ListView, so that when the keyboard appears your layout won’t get disrupted. However, in our example, we also want to place a FloatingActionButton at the bottom of the view. Technically we could place it under the ScrollView but 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 SingleChildScrollView with 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
with this:
Expanded(child: ContentCard()),

Finally, we can add those inputs, I think there is not much to explain here since this is all just a static view:
After adding it to ContentCard, we end up with this view:
You can see the full code for this step here.

. . .

3. Plane resize and travel

Let’s settle what is about to happen now. After a user clicks on a floating action button, the view immediately changes to another tab, however, the TabBar names 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:

Now, we will add PriceTab widget to the ContentCard :
Not so much for now, let’s add plane size animation. First by adding AnimationController and Animation objects related to the size of the plane.
So what we’re doing is creating in onInit() method an _planeSizeAnimationController which 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 AnimatedPlaneIcon widget which will be rebuilt on every animation change.
Great, now we can make this plane fly!
What we’ve done:

  • 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 _planeTopPadding getter so that it is dependent on _planeTravelAnimation.
  • We have added an AnimatedBuilder which 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.

Regarding update of labels, I won’t describe it here but you can check it out in the full code for this part.

EDIT: There was missing - _minPlanePaddingTop in the _maxPlaneTopPadding. Add it to move the plane a bit from the bottom edge.

. . .

4. Dots travel

In order to place dots, we need to know their positions. Obviously, they have to match cards on the sides. At this moment let’s assume that we have 4 cards, each will have a height of 80.0. We can also assume that those cards will overlap a bit (we will place cards in distance of 0.8*80.0).

We can start by creating a dot class

Now let’s add some things into PriceTab widget
Let me explain what happened. We created a list of flight stops which at this moment will be integers. We also introduced a fixed height of one stop which is 80.0. We created a _dotsAnimationController which 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 curve.

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.

You can see full code for this step here.

. . .

5. Flight stop card view

First, let’s create a model for a card, we will use strings because it’s easier. 🙂
Theoretically, this view is pretty simple and can be done with proper usage of Rows and Columns. However, if we look closely at the design, we can see that every text in the card shows up in a slightly different way. To achieve that we need to animate each text separately, so we need to know each text’s position. That’s why we will use stack here.
The first thing worth mentioning is isLeft flag. 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 height and width fields 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:

What we’ve done is depending on stop’s index we add an Expanded widget on the left or on the right of the card. Having the card also wrapped inside the Expanded we can make sure that it will take only half of the width.
You can see the full code for this step here.

. . .

6. Flight Stop Card animations

Ok, now it’s time to animate those cards! In order to do so, we will use a single AnimationController with multiple Interval animations just like with dots. We will also update getMargin methods so that they will be dependent on animation’s value.
We created an animation for each text we want to display. Every animation has slightly different interval values so that they create a feeling of being independent of each other. Also, we pass the parameter to ElasticOutCurve which 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 runAnimation so that parent can decide when the card should be animated.
In onInit() method we create GlobalKeys which we later pass to FlightStopCards so that we have access to the state and we can start the animations. We also added a FloatingActionButton so that whole view will be completed. This fab’s animation will start just after last card animation’s start. This time, we use Future.forEach to run a delayed animation start for each card. This is how it looks like:
You can see the full code for this step here.

. . .

7. Flight ticket view

Ok, now we’re left with the easy part. We need to create views for the last screen. Let’s start with the model since in the design there are completely different texts on the last screen.
Now let’s create a page widget:
Now let’s create a basic view of one ticket card:
It’s all pretty basic so I won’t discuss those static views. We still need to add navigate to this view. I will use FadeRoute to do that.
This is what we got at this moment:
As you can see, those cards are lacking circular cuts on the sides. Let’s add them with ClipPath:
Unfortunately, using `ClipPath` was also cutting off the shadow. In order to mitigate it a bit, I added a material with 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:
You can see the full code for this step here.

. . .

8. Flight ticket animations

This time we will use a lazier approach. To animate cards coming from the bottom, we can simply add a Translation from a big number to 0. I wouldn’t say it’s a perfect solution but it’s simple and it works.
As you can see, we used already known AnimationControllers, Animations and Intervals. I guess that at this point we don’t have to elaborate on that. If there is anything unclear, leave a question :).
You can see the full code for this step here.

. . .

And that’s it folks!

That was a long road down here but now we can compare the results:

The design

Implementation

I hope you enjoyed this post. If so, please leave a comment or star a repo 🙂

Cheers 🙂

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.

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!

Stripe Checkout in Flutter Web

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

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

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!

Share This

Share This

Share this post with your friends!