Development Challenge: Building a Collapsible AppBar in Flutter Part 2

Development Challenge: Building a Collapsible AppBar in Flutter Part 2

Improving on the collapsible app bar with animated leading and trailing icons.

Last week, I outlined a solution I came up with in building a collapsible app bar that has a background image with a blurring effect when the app bar is fully collapsed.

To further improve on, we will now add two actions buttons on this collapsible app bar, the first on the left will be a back button (using an arrow back icon) while the other will be an edit button (using the edit icon). The fun part is that we will animate these icons/buttons to change their behaviour based on the scrolling progress. When the flexible app bar is fully expended, with the background pictures "ublurred", it is useful to have a white border around the icons (to give them a contrast with the background and an elevated feel). However, when the app bar is fully collapsed and with a fully blurred background image, it is we can remove these white burders and use just the icons in the app bar. A sneak peak of the desired behaviour is shown below:

Collapsible App Bar With Changing Icons

The main idea to implement this is to first add an AppBar widget on top of the other widgets in the Stack widget that we have before in the Part 1 of this post. This AppBar widget will contain the leading and trailing buttons. To make the buttons change behaviour, we will choose a point between 0 and 1 (0.5 for example), which indicates the point during the scrolling progress at which the icons will change. In the code below, we update the Stack to contain the AppBar widget with both trailing and leading buttons with white borders. We all create and set the animationThreshold variable to 0.5.

MyPersistentSliverAppBar({
    required this.collapsedHeight,
    required this.expandedHeight,
  });

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    final progress = shrinkOffset / maxExtent;
    const Duration appBarAnimationDuration = Duration(milliseconds: 150);
    const double animationThreshold = 0.5;
    return Stack(
      clipBehavior: Clip.none,
      fit: StackFit.expand,
      children: [
        Container(
          height: maxExtent,
          clipBehavior:
              Clip.hardEdge, //prevent blurr from affecting whole screen
          decoration: const BoxDecoration(
            image: DecorationImage(
              image: NetworkImage('https://picsum.photos/seed/picsum/200/300'),
              fit: BoxFit.cover,
            ),
          ),
          child: BackdropFilter(
            filter: ImageFilter.blur(
              sigmaX: 90,
              sigmaY: 90,
            ),
            child: Container(
              color: Colors.black.withOpacity(0.3),
            ),
          ),
        ),
        AnimatedOpacity(
          duration: appBarAnimationDuration,
          opacity: 1 - progress,
          child: Image.network(
            'https://picsum.photos/seed/picsum/200/300',
            width: MediaQuery.of(context).size.width,
            fit: BoxFit.cover,
          ),
        ),
        // add an AppBar on top of the Stack
        AppBar(
          backgroundColor: Colors.transparent,
          leading: Container(
            decoration: const BoxDecoration(
              color: Colors.white, // Background color
              shape: BoxShape.circle,
            ),
            margin: const EdgeInsets.all(4), // Adjust the margin as needed
            child: IconButton(
              icon: const Icon(Icons.arrow_back, color: Colors.black),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ),
          actions: [
            Container(
              decoration: const BoxDecoration(
                color: Colors.white, // Background color
                shape: BoxShape.circle,
              ),
              margin: const EdgeInsets.all(4), // Adjust the margin as needed
              child: IconButton(
                icon: const Icon(Icons.edit_outlined, color: Colors.black),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ),
          ],
          elevation: 0,
        ),
      ],
    );
  }
  @override
  double get maxExtent => expandedHeight;
  @override
  double get minExtent => collapsedHeight;
  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
}

Now we just have to make the leading and trailing icons change based on the whether the scrolling progress is more than 0.5 or not. The full updated class is shown below:

class MyPersistentSliverAppBar extends SliverPersistentHeaderDelegate {
  final double expandedHeight;
  final double collapsedHeight;

  MyPersistentSliverAppBar({
    required this.collapsedHeight,
    required this.expandedHeight,
  });

  // final String _actionIconType = "elevated";

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    final progress = shrinkOffset / maxExtent;
    const Duration appBarAnimationDuration = Duration(milliseconds: 150);
    const double animationThreshold = 0.5;
    return Stack(
      clipBehavior: Clip.none,
      fit: StackFit.expand,
      children: [
        Container(
          height: maxExtent,
          clipBehavior:
              Clip.hardEdge, //prevent blurr from affecting whole screen
          decoration: const BoxDecoration(
            image: DecorationImage(
              image: NetworkImage('https://picsum.photos/seed/picsum/200/300'),
              fit: BoxFit.cover,
            ),
          ),
          child: BackdropFilter(
            filter: ImageFilter.blur(
              sigmaX: 90,
              sigmaY: 90,
            ),
            child: Container(
              color: Colors.black.withOpacity(0.3),
            ),
          ),
        ),
        AnimatedOpacity(
          duration: appBarAnimationDuration,
          opacity: 1 - progress,
          child: Image.network(
            'https://picsum.photos/seed/picsum/200/300',
            width: MediaQuery.of(context).size.width,
            fit: BoxFit.cover,
          ),
        ),
        AppBar(
          backgroundColor: Colors.transparent,
         // change button based on scroll progress
          leading: (1 - progress) <= animationThreshold
              ? IconButton(
                  icon: const Icon(Icons.arrow_back),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                  color: Colors.white,
                )
              : Container(
                  decoration: const BoxDecoration(
                    color: Colors.white, // Background color
                    shape: BoxShape.circle,
                  ),
                  margin: const EdgeInsets.all(4), // Adjust the margin as needed
                  child: IconButton(
                    icon: const Icon(Icons.arrow_back, color: Colors.black),
                    onPressed: () {
                      Navigator.of(context).pop();
                    },
                  ),
                ),
          actions: [
            // change button based on scroll progress
            (1 - progress) <= animationThreshold 
                ? IconButton(
                    icon: const Icon(Icons.edit_outlined),
                    onPressed: () {
                      Navigator.of(context).pop();
                    },
                    color: Colors.white,
                  )
                : Container(
                    decoration: const BoxDecoration(
                      color: Colors.white, 
                      shape: BoxShape.circle,
                    ),
                    margin: const EdgeInsets.all(4), 
                    child: IconButton(
                      icon: const Icon(Icons.edit_outlined, color: Colors.black),
                      onPressed: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
          ],
          elevation: 0,
        ),
      ],
    );
  }

  @override
  double get maxExtent => expandedHeight;

  @override
  double get minExtent => collapsedHeight;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
}

With this update, the leading and trailing button now changes based on the scrolling progress. What do you think this little hack?