In this article, I will guide you through the process of creating a frosted AppBar with a sliding element beneath it. The final result is presented at the top as it works in the media network application.
? Quick Access: GitHub Project
The idea was born
The initial idea was to create a SliverAppBar
with an expanded element. However, SliverAppBar
collapses when scrolling down, as shown in this video:
![](https://habrastorage.org/getpro/habr/upload_files/fe0/ccc/212/fe0ccc212fc40babe2353351578d0d84.gif)
Another concept that came to my mind was inserting horizontal dates inside the AppBar
here:
![](https://habrastorage.org/getpro/habr/upload_files/7af/87c/453/7af87c453002909176b82252d9d9cfb1.png)
But how to make it expand? Dynamically changing toolbarHeight
can be tedious especially when the precise height of those scrolling dates is unknown.
There is a possibility to hide the horizontal dates behind the AppBar and slide them down while scrolling. Here is the final stack for the sliding element. GlassFrostAppBar
will include scrolling dates and the SingleChildScrollView
will contain other elements, such as production, job offers, and more.
![](https://habrastorage.org/getpro/habr/upload_files/aad/b9c/4f9/aadb9c4f9996d9e0a55565a3aaa725a4.png)
Crafting the Magic
1. The Transparent AppBar
The first component will be AppBar
. It should be transparent, so the GlassFrostAppBar
behind could use a frosted effect.
appBar: AppBar(
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.dark, // For Android (dark icons)
statusBarBrightness: Brightness.light, // For iOS (dark icons)
),
scrolledUnderElevation: 0,
elevation: 0,
backgroundColor: Colors.transparent,
centerTitle: false,
title: Text('My Availability', style: context.textTheme.displaySmall),
),
2. Frosted Dates
Next will be the horizontal dates inside with a frosted effect — GlassFrostAppBar
. To create such an effect we will use the BackdropFilter
with ClipRect
.
import 'dart:ui';
import 'package:flutter/material.dart';
class GlassFrostAppBar extends StatelessWidget {
const GlassFrostAppBar({super.key});
@override
Widget build(BuildContext context) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.white.withOpacity(0.7)),
child: Stack(
children: [
Container(height: MediaQuery.of(context).padding.top),
],
),
),
),
);
}
}
Current result:
![](https://habrastorage.org/getpro/habr/upload_files/66b/6e4/2b4/66b6e42b4ce9de7803887429fe20fde1.png)
3. The Slide Effect
The next step involves creating the slide effect. Our dates will be moving from top to bottom. Also, we need to make them invisible at the top because the AppBar
is transparent and we don’t want to see these dates too early. We are going to need a Tween<Offset>
animation to do this.
class GlassFrostAppBar extends StatefulWidget {
const GlassFrostAppBar({super.key});
@override
State<GlassFrostAppBar> createState() => _GlassFrostAppBarState();
}
class _GlassFrostAppBarState extends State<GlassFrostAppBar> with TickerProviderStateMixin {
late AnimationController _expandController;
late Animation<Offset> animation;
@override
void dispose() {
_expandController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_expandController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
animation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(0, 1.5),
).animate(
CurvedAnimation(
parent: _expandController,
curve: Curves.fastOutSlowIn,
),
);
}
@override
Widget build(BuildContext context) {
...
}
}
4. The Slide Trigger
But how would we know when to slide down the dates? It should be right after the expanded dates scrolled behind the AppBar so that the user is always able to observe the dates. We must pass the ScrollController
of SingleChildScrollView
from the stack to the GlassFrostAppBar
. Then we need to add a listener to that scroll controller.
widget.mainScrollController.addListener(() {
if (widget.mainScrollController.offset > 140) {
_expandController.forward();
setState(() {
_isVisible = true;
});
} else {
_expandController.reverse();
setState(() {
_isVisible = false;
});
}
});
The _isVisible
variable ensured the dates remained concealed at the top behind the AppBar. Combining it with a sliding animation we have this result:
@override
Widget build(BuildContext context) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.white.withOpacity(0.7)),
child: Stack(
children: [
Container(height: MediaQuery.of(context).padding.top + (_isVisible ? 56 : 0)),
Visibility(
visible: _isVisible,
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: SlideTransition(
position: animation,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: HorizontalDates(isCollapsed: true),
),
),
),
),
],
),
),
),
);
}
Assembling all together and here is the outcome:
![](https://habrastorage.org/getpro/habr/upload_files/fb9/950/ff6/fb9950ff615dd5a36753e7a5863f465c.gif)
Looks bad … The dates appear to be visible on their way down and also they vanish abruptly on the ascent. The solution here could be to gradually make dates invisible with FadeTransition
. However, a better idea involves SizeTransition
animation. We can stick the dates at the bottom of the AppBar
and make them invisible by reducing their size to zero. This is how it will work:
![](https://habrastorage.org/getpro/habr/upload_files/bd6/f79/1b1/bd6f791b19b963a8c938eeb8ae4d73ce.png)
With SizeTransition
, we no longer require to toggle visibility. Here is the implementation:
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.white.withOpacity(0.7)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(height: MediaQuery.of(context).padding.top),
SizeTransition(
axisAlignment: 1,
sizeFactor: animation,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: HorizontalDates(isCollapsed: true),
),
),
],
),
),
),
);
And now, we can view the final result:
![](https://habrastorage.org/getpro/habr/upload_files/155/5ae/72e/1555ae72e43cb1695ad0156e4c7dd31a.png)
Thank you for reading! Dive into the complete code here: https://github.com/IlyaZadyabin/media