3
votes

I want to create a top app bar similar to the one in the Apple News app.

It's a bit difficult to see the blur in the screenshot because the bar is so thin, but you get the idea.

screenshot

I want the bar to expand and contract by scrolling and be pinned at the top when contracted, just like in the screenshot, the SliverAppBar does all that, except that I can't wrap it in ClipRect, BackdropFilter and Opacity to create the frosted glass effect because CustomScrollView only takes RenderSliver child classes.

My test code:

Widget build(BuildContext context) {
  return CustomScrollView(
    slivers: <Widget>[
      SliverAppBar(
        title: Text('SliverAppBar'),
        elevation: 0,
        floating: true,
        pinned: true,
        backgroundColor: Colors.grey[50],
        expandedHeight: 200.0,
        flexibleSpace: FlexibleSpaceBar(
            background: Image.network("https://i.imgur.com/cFzxleh.jpg", fit: BoxFit.cover)
        ),
      )
      ,
      SliverFixedExtentList(
        itemExtent: 150.0,
        delegate: SliverChildListDelegate(
          [
            Container(color: Colors.red),
            Container(color: Colors.purple),
            Container(color: Colors.green),
            Container(color: Colors.orange),
            Container(color: Colors.yellow),
            Container(color: Colors.pink,
              child: Image.network("https://i.imgur.com/cFzxleh.jpg", fit: BoxFit.cover)
            ),
          ],
        ),
      ),
    ],
  );
}

Is there a way to achieve what I want?

1

1 Answers

3
votes

I managed to get it working by wrapping an AppBar inside of a SliverPersistentHeader (this is basically what SliverAppBar does).

in action Ignore the un-blurred edges it's an iOS simulator bug.

Here is a proof of concept code example:

class TranslucentSliverAppBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SliverPersistentHeader(
        floating: true,
        pinned: true,
        delegate: _TranslucentSliverAppBarDelegate(
            MediaQuery.of(context).padding,
        )
    );
  }
}

class _TranslucentSliverAppBarDelegate extends SliverPersistentHeaderDelegate {

  /// This is required to calculate the height of the bar
  final EdgeInsets safeAreaPadding;

  _TranslucentSliverAppBarDelegate(this.safeAreaPadding);

  @override
  double get minExtent => safeAreaPadding.top;

  @override
  double get maxExtent => minExtent + kToolbarHeight;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return ClipRect(child: BackdropFilter(
      filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
      child: Opacity(
          opacity: 0.93,
          child: Container(
              // Don't wrap this in any SafeArea widgets, use padding instead
              padding: EdgeInsets.only(top: safeAreaPadding.top),
              height: maxExtent,
              color: Colors.white,
              // Use Stack and Positioned to create the toolbar slide up effect when scrolled up
              child: Stack(
                  overflow: Overflow.clip,
                  children: <Widget>[
                    Positioned(
                      bottom: 0, left: 0, right: 0,
                      child: AppBar(
                          primary: false,
                          elevation: 0,
                          backgroundColor: Colors.transparent,
                          title: Text("Translucent App Bar"),
                      ),
                    )
                  ],
              )
          )
      )
    ));
  }

  @override
  bool shouldRebuild(_TranslucentSliverAppBarDelegate old) {
    return maxExtent != old.maxExtent || minExtent != old.minExtent ||
        safeAreaPadding != old.safeAreaPadding;
  }
}