Custom shaped AppBar as seen in the “Bunny Search” app

Daria Orlova
Flutter Community
Published in
10 min readFeb 15, 2022

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:

  1. A gradient wave background
  2. Background transformation into a pinned app bar on scroll
  3. 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.

Something to keep in mind: ClipPath can be expensive.

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:

  1. Path starts at the origin (x: 0; y: 0), which is the top left corner.
  2. Then we draw a line from the origin, to the height of our size ⇒ to the point (x: 0; y: size.height)
  3. From that point, we draw a line to point (x: size.width, y: size.height)
  4. 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:

https://upload.wikimedia.org/wikipedia/commons/0/00/B%C3%A9zier_1_big.gif

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.

https://en.wikipedia.org/wiki/B%C3%A9zier_curve#/media/File:B%C3%A9zier_2_big.gif

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:

  1. Start it from ~0.75 of the height of the background
  2. End at ~0.5 height of the background
  3. 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 SliveListinside 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 buildmethod, 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 SliverSearchAppBarwhich is a SliverPersistentHeaderDelegate in the buildmethod 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:

  1. In the first case,
  2. when height = max = 280, we see the wave,
  3. then shrinkOffset = 0

When the shrinkOffset is zero, the contents will be rendered with a dimension of [maxExtent] in the main axis.

  1. In the second case,
  2. when height = min = 140, we see the collapsed SearchBar,
  3. 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 SliverPersistentHeaderDelegatedoesn’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:

  1. Extract the background wave into a separate widget called BackgroundWave
  2. Wrap it in a Stack
  3. 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.

--

--

Daria Orlova
Flutter Community

Flutter Developer @ChiliLabs • Tech writer, speaker & mentor • Co-founder of the Bunny Search app • Love traveling & animals. Especially cats!