{"id":364717,"date":"2024-05-21T02:22:50","date_gmt":"2024-05-21T02:22:50","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=364717"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=364717","title":{"rendered":"<span>Creating a Frosted AppBar in Flutter with a Slide-Down Widget<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>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.<\/p>\n<p>? Quick Access: <a href=\"https:\/\/github.com\/IlyaZadyabin\/media\" rel=\"noopener noreferrer nofollow\">GitHub Project<\/a><\/p>\n<h3>The idea was\u00a0born<\/h3>\n<p>The initial idea was to create a <code>SliverAppBar<\/code> with an expanded element. However, <code>SliverAppBar<\/code> collapses when scrolling down, as shown in this video:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/fe0\/ccc\/212\/fe0ccc212fc40babe2353351578d0d84.gif\" width=\"376\" height=\"400\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/fe0\/ccc\/212\/fe0ccc212fc40babe2353351578d0d84.gif\"\/><\/figure>\n<p>Another concept that came to my mind was inserting horizontal dates inside the <code>AppBar<\/code> here:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/7af\/87c\/453\/7af87c453002909176b82252d9d9cfb1.png\" width=\"521\" height=\"391\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7af\/87c\/453\/7af87c453002909176b82252d9d9cfb1.png\"\/><\/figure>\n<p>But how to make it expand? Dynamically changing <code>toolbarHeight<\/code><em> <\/em>can be tedious especially when the precise height of those scrolling dates is unknown.<\/p>\n<p>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. <code>GlassFrostAppBar<\/code> will include scrolling dates and the <code>SingleChildScrollView<\/code> will contain other elements, such as production, job offers, and more.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/aad\/b9c\/4f9\/aadb9c4f9996d9e0a55565a3aaa725a4.png\" width=\"800\" height=\"450\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/aad\/b9c\/4f9\/aadb9c4f9996d9e0a55565a3aaa725a4.png\"\/><\/figure>\n<h3>Crafting the\u00a0Magic<\/h3>\n<h4>1. The Transparent AppBar<\/h4>\n<p>The first component will be <code>AppBar<\/code>. It should be transparent, so the <code>GlassFrostAppBar<\/code> behind could use a frosted effect.<\/p>\n<pre><code class=\"dart\">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),       ),<\/code><\/pre>\n<h4>2. Frosted Dates<\/h4>\n<p>Next will be the horizontal dates inside with a frosted effect \u2014\u00a0<code>GlassFrostAppBar<\/code>. To create such an effect we will use the\u00a0<code>BackdropFilter<\/code>\u00a0with\u00a0<code>ClipRect<\/code><em>.<\/em><\/p>\n<div class=\"tm-iframe_temp\" data-src=\"https:\/\/embedd.srv.habr.com\/iframe\/6599b47d3add783089ac011b\" data-style=\"\" id=\"6599b47d3add783089ac011b\" width=\"\"><\/div>\n<pre><code class=\"dart\">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),             ],           ),         ),       ),     );   } }<\/code><\/pre>\n<p>Current result:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/66b\/6e4\/2b4\/66b6e42b4ce9de7803887429fe20fde1.png\" width=\"720\" height=\"720\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/66b\/6e4\/2b4\/66b6e42b4ce9de7803887429fe20fde1.png\"\/><\/figure>\n<h4>3. The Slide Effect<\/h4>\n<p>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\u00a0<code>AppBar<\/code>\u00a0is transparent and we don\u2019t want to see these dates too early. We are going to need a\u00a0<code>Tween&lt;Offset><\/code>\u00a0animation to do this.<\/p>\n<pre><code class=\"dart\">class GlassFrostAppBar extends StatefulWidget {   const GlassFrostAppBar({super.key});    @override   State&lt;GlassFrostAppBar> createState() => _GlassFrostAppBarState(); }  class _GlassFrostAppBarState extends State&lt;GlassFrostAppBar> with TickerProviderStateMixin {   late AnimationController _expandController;   late Animation&lt;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&lt;Offset>(       begin: Offset.zero,       end: const Offset(0, 1.5),     ).animate(       CurvedAnimation(         parent: _expandController,         curve: Curves.fastOutSlowIn,       ),     );   }      @override   Widget build(BuildContext context) {     ...   } }<\/code><\/pre>\n<h4>4. The Slide Trigger<\/h4>\n<p>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\u00a0<code>ScrollController<\/code><em>\u00a0<\/em>of<em>\u00a0<\/em><code>SingleChildScrollView<\/code><em>\u00a0<\/em>from the stack to the\u00a0<code>GlassFrostAppBar<\/code>. Then we need to add a listener to that scroll controller.<\/p>\n<pre><code class=\"dart\">widget.mainScrollController.addListener(() {       if (widget.mainScrollController.offset > 140) {         _expandController.forward();         setState(() {           _isVisible = true;         });       } else {         _expandController.reverse();         setState(() {           _isVisible = false;         });       }     });<\/code><\/pre>\n<p>The<strong><em>\u00a0<\/em><\/strong><code>_isVisible<\/code><em>\u00a0<\/em>variable ensured the dates remained concealed at the top behind the AppBar. Combining it with a sliding animation we have this result:<\/p>\n<pre><code class=\"dart\">@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),                     ),                   ),                 ),               ),             ],           ),         ),       ),     );   }<\/code><\/pre>\n<p>Assembling all together and here is the outcome:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/fb9\/950\/ff6\/fb9950ff615dd5a36753e7a5863f465c.gif\" width=\"864\" height=\"864\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/fb9\/950\/ff6\/fb9950ff615dd5a36753e7a5863f465c.gif\"\/><\/figure>\n<p><strong>Looks bad \u2026\u00a0<\/strong>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\u00a0<code>FadeTransition<\/code><em>.\u00a0<\/em>However, a better idea involves\u00a0<code>SizeTransition<\/code><em>\u00a0<\/em>animation. We can stick the dates at the bottom of the\u00a0<code>AppBar<\/code>\u00a0and make them invisible by reducing their size to zero. This is how it will work:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/bd6\/f79\/1b1\/bd6f791b19b963a8c938eeb8ae4d73ce.png\" width=\"720\" height=\"720\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/bd6\/f79\/1b1\/bd6f791b19b963a8c938eeb8ae4d73ce.png\"\/><\/figure>\n<p>With\u00a0<code>SizeTransition<\/code><em>,\u00a0<\/em>we no longer require to toggle visibility. Here is the implementation:<\/p>\n<pre><code class=\"dart\">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),                 ),               ),             ],           ),         ),       ),     );<\/code><\/pre>\n<p>And now, we can view the final result:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/155\/5ae\/72e\/1555ae72e43cb1695ad0156e4c7dd31a.png\" width=\"864\" height=\"864\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/155\/5ae\/72e\/1555ae72e43cb1695ad0156e4c7dd31a.png\"\/><\/figure>\n<p>Thank you for reading! Dive into the complete code here:\u00a0<a href=\"https:\/\/github.com\/IlyaZadyabin\/media\" rel=\"noopener noreferrer nofollow\"><u>https:\/\/github.com\/IlyaZadyabin\/media<\/u><\/a><\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/784944\/\"> https:\/\/habr.com\/ru\/articles\/784944\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>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.<\/p>\n<p>? Quick Access: <a href=\"https:\/\/github.com\/IlyaZadyabin\/media\" rel=\"noopener noreferrer nofollow\">GitHub Project<\/a><\/p>\n<h3>The idea was\u00a0born<\/h3>\n<p>The initial idea was to create a <code>SliverAppBar<\/code> with an expanded element. However, <code>SliverAppBar<\/code> collapses when scrolling down, as shown in this video:<\/p>\n<figure class=\"\"><\/figure>\n<p>Another concept that came to my mind was inserting horizontal dates inside the <code>AppBar<\/code> here:<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>But how to make it expand? Dynamically changing <code>toolbarHeight<\/code><em> <\/em>can be tedious especially when the precise height of those scrolling dates is unknown.<\/p>\n<p>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. <code>GlassFrostAppBar<\/code> will include scrolling dates and the <code>SingleChildScrollView<\/code> will contain other elements, such as production, job offers, and more.<\/p>\n<figure class=\"full-width\"><\/figure>\n<h3>Crafting the\u00a0Magic<\/h3>\n<h4>1. The Transparent AppBar<\/h4>\n<p>The first component will be <code>AppBar<\/code>. It should be transparent, so the <code>GlassFrostAppBar<\/code> behind could use a frosted effect.<\/p>\n<pre><code class=\"dart\">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),       ),<\/code><\/pre>\n<h4>2. Frosted Dates<\/h4>\n<p>Next will be the horizontal dates inside with a frosted effect \u2014\u00a0<code>GlassFrostAppBar<\/code>. To create such an effect we will use the\u00a0<code>BackdropFilter<\/code>\u00a0with\u00a0<code>ClipRect<\/code><em>.<\/em><\/p>\n<div class=\"tm-iframe_temp\" data-src=\"https:\/\/embedd.srv.habr.com\/iframe\/6599b47d3add783089ac011b\" data-style=\"\" id=\"6599b47d3add783089ac011b\" width=\"\"><\/div>\n<pre><code class=\"dart\">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),             ],           ),         ),       ),     );   } }<\/code><\/pre>\n<p>Current result:<\/p>\n<figure class=\"full-width\"><\/figure>\n<h4>3. The Slide Effect<\/h4>\n<p>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\u00a0<code>AppBar<\/code>\u00a0is transparent and we don\u2019t want to see these dates too early. We are going to need a\u00a0<code>Tween&lt;Offset><\/code>\u00a0animation to do this.<\/p>\n<pre><code class=\"dart\">class GlassFrostAppBar extends StatefulWidget {   const GlassFrostAppBar({super.key});    @override   State&lt;GlassFrostAppBar> createState() => _GlassFrostAppBarState(); }  class _GlassFrostAppBarState extends State&lt;GlassFrostAppBar> with TickerProviderStateMixin {   late AnimationController _expandController;   late Animation&lt;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&lt;Offset>(       begin: Offset.zero,       end: const Offset(0, 1.5),     ).animate(       CurvedAnimation(         parent: _expandController,         curve: Curves.fastOutSlowIn,       ),     );   }      @override   Widget build(BuildContext context) {     ...   } }<\/code><\/pre>\n<h4>4. The Slide Trigger<\/h4>\n<p>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\u00a0<code>ScrollController<\/code><em>\u00a0<\/em>of<em>\u00a0<\/em><code>SingleChildScrollView<\/code><em>\u00a0<\/em>from the stack to the\u00a0<code>GlassFrostAppBar<\/code>. Then we need to add a listener to that scroll controller.<\/p>\n<pre><code class=\"dart\">widget.mainScrollController.addListener(() {       if (widget.mainScrollController.offset > 140) {         _expandController.forward();         setState(() {           _isVisible = true;         });       } else {         _expandController.reverse();         setState(() {           _isVisible = false;         });       }     });<\/code><\/pre>\n<p>The<strong><em>\u00a0<\/em><\/strong><code>_isVisible<\/code><em>\u00a0<\/em>variable ensured the dates remained concealed at the top behind the AppBar. Combining it with a sliding animation we have this result:<\/p>\n<pre><code class=\"dart\">@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),                     ),                   ),                 ),               ),             ],           ),         ),       ),     );   }<\/code><\/pre>\n<p>Assembling all together and here is the outcome:<\/p>\n<figure class=\"full-width\"><\/figure>\n<p><strong>Looks bad \u2026\u00a0<\/strong>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\u00a0<code>FadeTransition<\/code><em>.\u00a0<\/em>However, a better idea involves\u00a0<code>SizeTransition<\/code><em>\u00a0<\/em>animation. We can stick the dates at the bottom of the\u00a0<code>AppBar<\/code>\u00a0and make them invisible by reducing their size to zero. This is how it will work:<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>With\u00a0<code>SizeTransition<\/code><em>,\u00a0<\/em>we no longer require to toggle visibility. Here is the implementation:<\/p>\n<pre><code class=\"dart\">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),                 ),               ),             ],           ),         ),       ),     );<\/code><\/pre>\n<p>And now, we can view the final result:<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>Thank you for reading! Dive into the complete code here:\u00a0<a href=\"https:\/\/github.com\/IlyaZadyabin\/media\" rel=\"noopener noreferrer nofollow\"><u>https:\/\/github.com\/IlyaZadyabin\/media<\/u><\/a><\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/784944\/\"> https:\/\/habr.com\/ru\/articles\/784944\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-364717","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/364717","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=364717"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/364717\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=364717"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=364717"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=364717"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}