Custom shaped AppBar as seen in the “Bunny Search” app
whoami
Well hello there! Before we dive in, let me quickly introduce myself and what this tutorial is about. My name is Daria, I’m a Flutter developer at Chili Labs. I’m also the co-founder (oh, fancy 😂) and the only developer of the “Bunny Search” app.
…and what is Bunny Search?
“Bunny Search” is an app for searching cruelty-free brands. It is an aggregator of organizations, that certify brands with cruelty-free status (e.g. PETA) as well as our own collection of cruelty-free brands. Our own = verified by our team via communicating straight with the brands. Our team is just three people — Yana, the communications and brand guru, Alexandra, the talent behind the app’s design, and me, the developer 😁 We work on this app in our free time and for free, with no ads, affiliation links, etc. We plan to keep it that way because our sole goal with this app is to help animals by making the “cruelty-free” choice easier for the consumers.
About this tutorial
Anyway, since you’re here for the technical part, let’s finally jump into it 😁
In this tutorial, I want to show you how I created a cute scroll effect. Maybe it will inspire you to do something similar in your own app 🙂
Oh, and a bit about expectations: I assume you are familiar at least a bit with Flutter since I won’t be explaining the basics about what is a widget, how to create an app, how to style it, etc.
TLDR;
If you just want to see the code, here is the GitHub link: https://github.com/darjaorlova/bunny_search_animated_searchbar
The “cute scroll” effect
There are a few things going on here:
- A gradient wave background
- Background transformation into a pinned app bar on scroll
- Fading text and image on scroll
Let’s go through it step by step!
Step 1: A gradient wave background
Step 1.1: A gradient background
Let’s begin by creating a basic app with a Container. The width should match the device width and the height will be fixed 280. And to fill it in with a gradient we just need to add a decoration
to the container.
And it will look like this:
Before we move on to the wave part, let’s do the same gradient background, but with a different widget, that will eventually help us create the wave.
We will use a widget called ClipPath
. According to the docs, a ClipPath
is A widget that clips its child using a path.
😁 Thanks, cap. In my own words, this is a type of widget that allows you to cut out anything that you like from the child widget. There are specific ClipRect
, ClipOval
, etc., that allow you to explicitly cut (clip) out rectangles and ovals, but since our shape will be custom, we need a Path
.
ClipPath accepts a param called clipper. To create a clipper, we need to override a class called CustomClipper.
Now we need to override just 2 methods, shouldReclip
and Path getClip(Size size)
. This is where all of the clipping is done. First, let’s just clip the same rectangle that we already have.
What this does is:
Path
starts at the origin(x: 0; y: 0)
, which is the top left corner.- Then we draw a line from the origin, to the height of our
size
⇒ to the point(x: 0; y: size.height)
- From that point, we draw a line to point
(x: size.width, y: size.height)
- And then we close the path from our current point to the origin.
And so if we wrap our gradient Container
with a ClipPath
and pass BackgroundWaveClipper
as the clipper
, then nothing should change in the UI.
Step 1.2: A wave background
Now let’s get to the fun part 😁 For starters, let’s make our rectangle a bit more interesting. How about a diagonal baseline? For that we need to change just a tiny piece of code:
And it will look like this:
Ok, cool… but still a bit far from what we want, right?
How to draw a curvy line?
Uff, I’m not sure how to explain this one easily 😅 But I will give it a try.
In computer graphics, there is a notion called the “Bézier curve”. In very simple words, this is a function, which can accept a variable amount of points, and then draw a curvy line based on them.
Let’s look at the example from Wikipedia.
Linear curve
Let’s say we have 2 points: a starting point P0
and an endpoint P1
. Then our curve will be just a straight line:
Now, this is pretty straightforward (pun intended 😂). Let’s try something fancier.
Quadratic curve
We will add one more point in between, hence P0
will be the starting point, P2
will be the endpoint, and P1
will be a so-called control point
. So what will happen is that we will draw a line from P0
to P2
, but it will be “attracted” to P1
and hence form a curve. I think the image will make it clearer.
If you want to understand the maths and details behind it, you can go ahead and research, but for the sake of this tutorial understanding, this conceptually will be enough. Let’s get back to code.
So far we’ve been using the lineTo
method of the path
. To draw the curve we will use the method quadraticBezierTo
. From the docs we can see that it works the same we have just discussed:
- accepts a control point,
- accepts an endpoint,
- and uses our current point as the starting point to draw a curve.
/// Adds a quadratic bezier segment that curves from the current
/// point to the given point (x2,y2), using the control point
/// (x1,y1). void quadraticBezierTo(double x1, double y1, double x2, double y2)
Let’s first visualize how we want the curve to go:
- Start it from ~0.75 of the height of the background
- End at ~0.5 height of the background
- And curve towards a slight left of the center of the width
And translate it into code:
And let’s run it:
Awesome, we’ve got the curve! Now we’re ready for the next part: make it scrollable.
Step 2: A scrollable SearchBar
Step 2.1 A scrollable background
If you have worked with Flutter, you’ve probably already encountered slivers. But in case you haven’t, here’s a quick intro. According to the docs:
A sliver is a portion of a scrollable area that you can define to behave in a special way. You can use slivers to achieve custom scrolling effects, such as elastic scrolling.
In a really abstract way: there is a bunch of widgets with a Sliver
prefix which you can combine with each other to create interesting scrolling effects. In our case, it’s a scrolling SearchBar and a List that goes behind it. Here’s a quick “Widget of the week” video about SliverList
.
And let’s actually use it. We’ll put a SliveList
inside a CutsomScrollView
and supply a bunch of very simple children via SliverChildBuilderDelegate
delegate.
Let’s look at it:
Ok, nothing fancy… just a list of texts. Precisely what we told it to be 😁 And the list goes behind the status bar because it is not inside a SafeArea
.
So what we want is to add the gradient wave before the list and make the list scroll behind it. For that, we will use a widget called SliverPersistentHeader
, which requires a parameter delegate
of type SliverPersistentHeaderDelegate
. That parameter is used to describe the max & min size of the SliverPersistentHeader
, as well as how to build the actual widget.
It will be easier to understand from code, so let’s implement it:
As we already discussed, our height will be 280, so maxExtent = 280. Then let’s make the collapsed height half of that → 140. And in the build
method, we will ignore all of the params for now and just return what we have already created ⇒ in the gradient wave.
And now if we put it inside of our CustomScrollView
before the SliveList
Let’s take a look at the result:
Now, this looks much closer to what we want! But still not exactly, because we want the wave to become a straight line when it reaches its min size.
So basically we have 2 states: when the height = max = 280, then we show a wave. When the height = min = 140, then we show a rectangle. And animate between these two positions.
With a little bit of math, I came up with this:
And if we run this code:
Now, this looks great 😎 What’s left is to actually add the host of this whole party — the search bar 😁
Step 2.2 A scrolling SearchBar
First of all, let’s create the actual SearchBar. I won’t be going into much detail because this is just a decorated TextFormField.
And it will look like this:
We again have two positions:
- the initial one, when the wave is expanded
- the final one, when SearchBar is pinned.
In our SliverSearchAppBar
which is a SliverPersistentHeaderDelegate
in the build
method we have a param called shrinkOffset
. According to the documentation:
The `shrinkOffset` is a distance from [maxExtent] towards [minExtent]representing the current amount by which the sliver has been shrunk. When the `shrinkOffset` is zero, the contents will be rendered with a dimension of [maxExtent] in the main axis. When `shrinkOffset` equals the difference between [maxExtent] and [minExtent] (a positive number), the contents will be rendered with a dimension of [minExtent] in the main axis. The `shrinkOffset` will always be a positive number in that range.
This is how I understand it:
- In the first case,
- when height = max = 280, we see the wave,
- then
shrinkOffset
= 0
When the
shrinkOffset
is zero, the contents will be rendered with a dimension of [maxExtent] in the main axis.
- In the second case,
- when height = min = 140, we see the collapsed SearchBar,
- then
shrinkOffset
= 140 (max - min ⇒ 280 - 140 ⇒ 140).
When
shrinkOffset
equals the difference between [maxExtent] and [minExtent] (a positive number), the contents will be rendered with a dimension of [minExtent] in the main axis.
With this in mind I came up with a formula:
double offset = (maxExtent - minExtent - shrinkOffset) * 0.5;
1. When height = max = 280,
then shrinkOffset = 0,
then offset = (280 - 140 - 0) * 0.5 = 70; 2. When height = min = 140,
then shrinkOffset = max - min = 280 - 140 = 140,
then offset = (280 - 140 - 140) * 0.5 = 0; P.S. Since our max - min = min = 140 we can remove the maxExtent - minExtent out of the formula, so it will be double offset = (minExtent - shrinkOffset) * 0.5;
But in practice, I learned that despite what the documentation states in this place, the actual value range of shrinkOffset
is not [0, max-min]
, but [0, max]
. I’m not sure why, since the actual height of the SliverPersistentHeaderDelegate
doesn’t change after shrinkOffset
reaches the value of max — min
, but oh well.
var adjustedShrinkOffset = shrinkOffset > minExtent ? minExtent : shrinkOffset;
double offset = (minExtent - adjustedShrinkOffset) * 0.5;
A visualization for better comprehension:
And if we put it all together in code:
- Extract the background wave into a separate widget called
BackgroundWave
- Wrap it in a
Stack
- Position our freshly created
SearchBar
according to the calculations we just discussed
And voila! We’ve finally finished it 😁
P.S. — You can avoid this hack by adjusting shrinkOffset
by using NestedScrollView as described in the “Sample in an App” section here, but this is already a bit out of the scope 😁
P.P.S. — The fading text and image are just Positioned
the same way as SearchBar
. They’re both placed inside AnimatedOpacity
and the opacity
param is based on the shrinkOffset
. If shrinkOffset
is more than a specific value, then opacity = 0.
You can check out the result on Github: https://github.com/darjaorlova/bunny_search_animated_searchbar
Thanks for reading! If you like the effect and enjoyed the tutorial, I’d be happy to see your claps, comments, feedback, and follows! You can also find me on Twitter — @dariadroid.
Download Bunny Search App:
Android: https://play.google.com/store/apps/details?id=lv.chi.bunny_search
iOS: https://apps.apple.com/lv/app/bunny-search/id1592571643
More awesome articles on the topic:
- https://medium.com/flutter-community/clipping-in-flutter-e9eaa6b1721a
- https://medium.com/flutter-community/paths-in-flutter-a-visual-guide-6c906464dcd0
- https://iiro.dev/clipping-widgets-with-bezier-curves-in-flutter/
- https://jasper-dev.hashnode.dev/drawing-bezier-curves-and-splines-with-custompaint-flutter
- https://medium.com/flutter-community/flutter-custom-clipper-28c6d380fdd6
- https://www.fluttercampus.com/guide/36/how-to-make-bezier-curve-waves-using-custom-clip-path-in-flutter/
- https://medium.com/@TakRutvik/flutter-design-challenge-spotify-album-scroll-interaction-df6845debd1f
Daria 💙