How To Create Custom AppBar With CustomPaint In Flutter

Appliable
5 min readSep 13, 2020
Designed by HoangPts

Introduction

So, you have tried out the standard AppBar widget where you probably played around with a couple of things like title, background color, and action widgets. But let’s suppose you got bored and decided to make your very own AppBar to stand out. Hopefully, this article will help you out with that.

Understanding CustomPaint and CustomPainter

OK, what is CustomPaint, and why it’ll pay off to learn? Let’s say you want to make a custom widget with complicated curves, diverse layers, and custom animations on top of that. Sounds great, isn’t it? But before implementing your very own AppBar, let’s have a look at what CustomPaint offers. This is how its constructor looks like:

CustomPaint({Key key, CustomPainter painter,
CustomPainter foregroundPainter,
Size size: Size.zero,
bool isComplex: false,
bool willChange: false,
Widget child
}

As you see, CustomPaint constructor has multiple parameters:

  • painter — the painter that paints before the children.
  • foregroundPainter — the painter that paints after the children,
  • size — a size which CustomPaint aims for.
  • isComplex — tells whether the painter complex enough to benefit from caching.
  • willChange — defines if the paint will be changed in the future.
  • child — the widget below this widget tree.

For our needs, it’s enough to understand how the painter parameter works. And that leads us to the new widget called CustomPainter that will do the job we want. This is how CustomPainter class implementation looks like:

class CustomShape extends CustomPainter {
CustomShape();

@override
void paint(Canvas canvas, Size size)

@override
bool shouldRepaint(CustomPainter oldDelegate)
}

As you notice, we have two important methods up there. The first method is called paint. It takes two parameters:

  • Canvas — is an object that represents an area where we can paint any creative form we can imagine.
  • Size — defines the size of a box area we are going to paint on.

The second one is called shouldRepaint. It specifies whether we want a repaint a shape when its instance is changed.

Before we get started, there is another thing better to mention — Paint object. Basically, the main goal of this object is to draw strokes, but in conjunction with canvas, it’s possible to draw any sophisticated forms we can ever imagine. Here is an example:

@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTRB(50, 100, 250, 200);
final startAngle = -math.pi / 2;
final sweepAngle = math.pi;
final useCenter = false;
final paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 4;
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
}
  • The rect — specifies an area to draw custom shape within.
  • The startAngle — defines a location to paint from.
  • The sweepAngle — is how much degrees you want to draw from startAngle.
  • The useCenter — If true, the arc is closed back to the center, forming a circle sector.
  • Thepaint — determines parameters of stroke.
  • Finally, canvas.drawArc method — renders shape on the canvas.

Implementation

CustomToolbarShape class:

class CustomToolbarShape extends CustomPainter {
final Color lineColor;

const CustomToolbarShape({this.lineColor});

@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint();

//First oval
Path path = Path();
Rect pathGradientRect = new Rect.fromCircle(
center: new Offset(size.width / 4, 0),
radius: size.width/1.4,
);

Gradient gradient = new LinearGradient(
colors: <Color>[
Color.fromRGBO(225, 89, 89, 1).withOpacity(1),
Color.fromRGBO(255, 128, 16, 1).withOpacity(1),
],
stops: [
0.3,
1.0,
],
);

path.lineTo(-size.width / 1.4, 0);
path.quadraticBezierTo(
size.width / 2, size.height * 2, size.width + size.width / 1.4, 0);

paint.color = Colors.deepOrange;
paint.shader = gradient.createShader(pathGradientRect);
paint.strokeWidth = 40;
path.close();

canvas.drawPath(path, paint);

//Second oval
Rect secondOvalRect = new Rect.fromPoints(
Offset(-size.width / 2.5, -size.height),
Offset(size.width * 1.4, size.height / 1.5),
);

gradient = new LinearGradient(
colors: <Color>[
Color.fromRGBO(225, 255, 255, 1).withOpacity(0.1),
Color.fromRGBO(255, 206, 31, 1).withOpacity(0.2),
],
stops: [
0.0,
1.0,
],
);
Paint secondOvalPaint = Paint()
..color = Colors.deepOrange
..shader = gradient.createShader(secondOvalRect);

canvas.drawOval(secondOvalRect, secondOvalPaint);

//Third oval
Rect thirdOvalRect = new Rect.fromPoints(
Offset(-size.width / 2.5, -size.height),
Offset(size.width * 1.4, size.height / 2.7),
);

gradient = new LinearGradient(
colors: <Color>[
Color.fromRGBO(225, 255, 255, 1).withOpacity(0.05),
Color.fromRGBO(255, 196, 21, 1).withOpacity(0.2),
],
stops: [
0.0,
1.0,
],
);
Paint thirdOvalPaint = Paint()
..color = Colors.deepOrange
..shader = gradient.createShader(thirdOvalRect);

canvas.drawOval(thirdOvalRect, thirdOvalPaint);

//Fourth oval
Rect fourthOvalRect = new Rect.fromPoints(
Offset(-size.width / 2.4, -size.width/1.875),
Offset(size.width / 1.34, size.height / 1.14),
);

gradient = new LinearGradient(
colors: <Color>[
Colors.red.withOpacity(0.9),
Color.fromRGBO(255, 128, 16, 1).withOpacity(0.3),
],
stops: [
0.3,
1.0,
],
);
Paint fourthOvalPaint = Paint()
..color = Colors.deepOrange
..shader = gradient.createShader(fourthOvalRect);

canvas.drawOval(fourthOvalRect, fourthOvalPaint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

Great! Now, let’s put it in our custom AppBar which extends SliverPersistentHeaderDelegate.

CustomAppBar class:

class CustomAppBar extends SliverPersistentHeaderDelegate {
final double height;

const CustomAppBar({this.height});

@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: Colors.transparent,
child: Stack(fit: StackFit.loose, children: <Widget>[
Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
height: height,
child: CustomPaint(
painter: CustomToolbarShape(lineColor: Colors.deepOrange),
));
}

@override
double get maxExtent => height;

@override
double get minExtent => height;

@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}

As you see, in paint method, we draw one shape after another, which results in this:

CustomAppBar shape

Now, let’s add missing action buttons and search text field. To do this we should use Align widget.

class CustomAppBar extends SliverPersistentHeaderDelegate {
final double height;

const CustomAppBar({this.height});

@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: Colors.transparent,
child: Stack(fit: StackFit.loose, children: <Widget>[
Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
height: height,
child: CustomPaint(
painter: CustomToolbarShape(lineColor: Colors.deepOrange),
)),
Align(
alignment: Alignment(0.0, 1.25),
child: Container(
height: MediaQuery.of(context).size.height / 14.5,
padding: EdgeInsets.only(left: 30, right: 30),
child: Container(
decoration: new BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 20.0,
// shadow
spreadRadius: .5,
// set effect of extending the shadow
offset: Offset(
0.0,
5.0,
),
)
],
),
child: TextField(
onSubmitted: (submittedText) {

},
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
prefixIcon: Icon(
Icons.search,
color: Colors.black38,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white, width: 1), borderRadius: BorderRadius.circular(25)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white, width: 1), borderRadius:
BorderRadius.circular(25))))))),
Align(
alignment: Alignment(0.9, 0.0),
child: Container(
height: MediaQuery.of(context).size.height / 13,
width: MediaQuery.of(context).size.width / 13,
child: InkWell(
onTap: () {
},
child: Icon(
Icons.local_mall,
color: Colors.black,
),
))),
Align(
alignment: Alignment(-0.9, 0.0),
child: Container(
height: MediaQuery.of(context).size.height / 13,
width: MediaQuery.of(context).size.width / 13,
child: InkWell(
onTap: () {
},
child: Icon(
Icons.menu,
color: Colors.black,
)))),
])
);
}

@override
double get maxExtent => height;

@override
double get minExtent => height;

@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}

Result

Final Result

In case you have any questions, hit us a line at link.

--

--

Appliable

We strive for #ambient and #innovative products. More at appliable.eu