flutterflutter-sliverflutter-sliverappbar

Flutter: How to add BackdropFilter to SliverAppBar


I want to add a BackdropFilter() to a SliverAppbar().

I want it to look something like the iOS App Library App Bar: https://cln.sh/eP8wfY.

Header sliver not floating over the list in a NestedScrollView does so but only to the header, I want the title and the actions to be visible while the background is blurred.

Thanks!

Edit

What the pages look like: https://cln.sh/vcCY4j.

Github Gist with my code: https://gist.github.com/HadyMash/21e7bd2f7e202de02837505e1c7363e9.


Solution

  • TL;DR I fixed the problem by using a normal AppBar() since I didn't need a SliverAppBar(). I made a custom app bar to fix the problem (see code at the end of the question).


    I realised I didn't need a SilverAppBar() because it will just stay floating and pinned. This made my life a whole lot easier since I could use an AppBar() and set the extendBodyBehindAppBar to true in the Scaffold(). This made it so that I wouldn't have to make a custom sliver widget as I am not familiar with making them.

    My solution was to make a custom AppBar(). I would have a Stack() then put the blur effect and the AppBar() above it.

    https://github.com/flutter/flutter/issues/48212 shows that you can't use ShaderMasks() with BackdropFilter()s. To work around this I made a column with a bunch of BackdropFilter()s. They would have decreasing sigma values to create the gradient effect I was looking for. This isn't very performant, however, and in heavier apps wouldn't work well. Making each block have the length of a single logical pixel was too heavy so I made it 2 logical pixels.

    It can also be easily expanded, for example, by adding a fade effect as I did.

    Here is what the result looks like.

    Here is the code for the solution:

    import 'dart:math';
    import 'dart:ui';
    import 'package:flutter/material.dart';
    
    class BlurredAppBar extends StatelessWidget implements PreferredSizeWidget {
      final String title;
      final List<Widget>? actions;
    
      /// An `AppBar()` which has a blur effect behind it which fades in to hide it
      /// until content appears behind it. This has a similar effect to the iOS 14
      /// App Library app bar. It also has the possibility of having a fade effect to
      /// redude the opacity of widgets behind the `BlurredAppBar()` using a `LinearGradient()`.
      const BlurredAppBar({required this.title, this.actions, Key? key})
          : super(key: key);
    
      /// The height of the `AppBar()`
      final double height = 56;
    
      /// Returns a `List<Widget>` of `BackdropFilter()`s which have decreasing blur values.
      /// This will create the illusion of a gradient blur effect as if a `ShaderMask()` was used.
      List<Widget> _makeBlurGradient(double height, MediaQueryData mediaQuery) {
        List<Widget> widgets = [];
        double length = height + mediaQuery.padding.top;
    
        for (int i = 1; i <= (length / 2); i++) {
          widgets.add(
            ClipRRect(
              child: BackdropFilter(
                filter: ImageFilter.blur(
                  sigmaX: max(((length / 2) - i.toDouble()) / 2, 0),
                  sigmaY: min(5, max(((length / 2) - i.toDouble()) / 2, 0)),
                ),
                child: SizedBox(
                  height: 2,
                  width: mediaQuery.size.width,
                ),
              ),
            ),
          );
        }
    
        return widgets;
      }
    
      @override
      Widget build(BuildContext context) {
        final MediaQueryData mediaQuery = MediaQuery.of(context);
    
        return Stack(
          children: [
            // BackdropFilters
            SizedBox(
              height: height + mediaQuery.padding.top,
              child: Column(
                children: _makeBlurGradient(height, mediaQuery),
              ),
            ),
            // Fade effect.
            Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  stops: [0.5, 1],
                  colors: [
                    Colors.white.withOpacity(0.8),
                    Colors.white.withOpacity(0),
                  ],
                ),
              ),
            ),
    
            // AppBar
            AppBar(
              title: Text(
                title,
                style: Theme.of(context).textTheme.headline3,
              ),
              automaticallyImplyLeading: false,
              actions: actions,
            ),
          ],
        );
      }
    
      @override
      Size get preferredSize => Size.fromHeight(height);
    }