SliverPersistentHeaderDelegate should collapse before the Remaining widgets scroll under it

671 Views Asked by At

I am trying to make SliverPersistentHeaderDelegate to collapse first and then the rest of the screen to scroll.

Here is the code.

class MyHomePage extends StatelessWidget {
  const MyHomePage({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        physics: const PageScrollPhysics(),
        slivers: [
          SliverPersistentHeader(
            pinned: true,
            delegate: SliverAppBarDelegate(),
          ),
          const SliverToBoxAdapter(
            child: SizedBox(height: 24),
          ),
          SliverPersistentHeader(
            pinned: true,
            delegate: SliverGreenMenuBarDelegate(),
          ),
          const SliverPadding(padding: EdgeInsets.only(bottom: 15)),
          SliverFillRemaining(
            child: SizedBox(
              height: 200,
              child: Container(
                color: Colors.amber,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  SliverAppBarDelegate();

  var expandedHeight = 220.0;

  @override
  double get maxExtent => expandedHeight;

  @override
  double get minExtent => kToolbarHeight + 20;

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    var colorScheme = Theme.of(context).colorScheme;

    double radius() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 20.0;
      } else {
        return 20 - shrinkOffset / 11;
      }
    }

    final Widget appBar = FlexibleSpaceBar.createSettings(
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      child: AppBar(
        actions: [
          IconButton(
            padding: const EdgeInsets.all(0),
            constraints: const BoxConstraints(),
            onPressed: () {},
            icon: const Icon(Icons.abc),
          ),
          Padding(
            padding: const EdgeInsets.only(right: 16, left: 35),
            child: IconButton(
              padding: const EdgeInsets.all(0),
              constraints: const BoxConstraints(),
              onPressed: () {},
              icon: const Icon(Icons.ac_unit_sharp),
            ),
          ),
        ],
        flexibleSpace: const FlexibleSpaceBar(
          expandedTitleScale: 1,
          titlePadding: EdgeInsets.only(top: 4, left: 56, right: 56),
          title: SafeArea(
            child: Text("Hello world"),
          ),
        ),
        elevation: 10,
        shadowColor: colorScheme.secondary.withOpacity(0.2),
        primary: true,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.only(
            bottomLeft: Radius.circular(radius()),
            bottomRight: Radius.circular(radius()),
          ),
        ),
      ),
    );
    return appBar;
  }
}

class SliverGreenMenuBarDelegate extends SliverPersistentHeaderDelegate {
  @override
  double get maxExtent => 75.0;

  @override
  double get minExtent => 40.0;

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    double horizontalPadding() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 25.0;
      } else {
        return 25 - (shrinkOffset / 3);
      }
    }

    double cornerRadius() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 15.0;
      } else {
        return 15 - shrinkOffset / 5;
      }
    }

    return FlexibleSpaceBar.createSettings(
      maxExtent: maxExtent,
      minExtent: minExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      isScrolledUnder: true,
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: horizontalPadding()),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.green,
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(cornerRadius()),
              topRight: Radius.circular(cornerRadius()),
              bottomLeft: const Radius.circular(15),
              bottomRight: const Radius.circular(15),
            ),
          ),
        ),
      ),
    );
  }
}

And here is the video. enter image description here

Basically, what I want is the green part to collapse first and the yellow part to go under it.

How can I achieve this?

The end result should be as shown in the screenshots below.

Before Scroll After scrool
enter image description here enter image description here

Thank you in advance for the help!

1

There are 1 best solutions below

0
On
  • i have added an AnimationController and an Animation to handle the animation. also a ScrollController to monitor the scroll position.

  • Inside the initState method, i initialize the animation controller and define the animation range using a Tween.

  • The Tween defines the beginning and ending values for the animation. i also set up a listener on the scroll controller to determine whether to play the animation forward or reverse based on the scroll offset.


  import 'package:flutter/material.dart';
    
    class DiffSliver extends StatefulWidget {
      const DiffSliver({Key? key}) : super(key: key);
    
      @override
      State<DiffSliver> createState() => _DiffSliverState();
    }
    
    class _DiffSliverState extends State<DiffSliver>
        with SingleTickerProviderStateMixin {
      late AnimationController _animationController;
      late Animation<double> _animation;
      final ScrollController _scrollController = ScrollController();
    
      @override
      void initState() {
        super.initState();
        _animationController = AnimationController(
          vsync: this,
          duration: Duration(milliseconds: 250),
        );
        _animation = Tween<double>(begin: 0.0, end: 1.0).animate(
          CurvedAnimation(
            parent: _animationController,
            curve: Curves.easeInOut,
          ),
        );
    
        _scrollController.addListener(() {
          if (_scrollController.offset > 150) {
            _animationController.forward();
          } else {
            _animationController.reverse();
          }
        });
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        _scrollController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.white,
          body: SafeArea(
            child: NestedScrollView(
              controller: _scrollController,
              headerSliverBuilder: (context, innerBoxIsScrolled) {
                return [
                  SliverAppBar(
                    pinned: true,
                    expandedHeight: 200,
                    flexibleSpace: FlexibleSpaceBar(
                      background: Container(
                        width: 100,
                        height: 100,
                        color: Colors.blue,
                      ),
                    ),
                  ),
                  SliverPersistentHeader(
                    pinned: true,
                    delegate: AnimatedContainerDelegate(animation: _animation),
                  ),
                  SliverPersistentHeader(
                    pinned: true,
                    delegate: ContainerDelegate(),
                  )
                ];
              },
              body: Container(
                color: Colors.white,
              ),
            ),
          ),
        );
      }
    }
    
    class ContainerDelegate extends SliverPersistentHeaderDelegate {
      @override
      Widget build(
          BuildContext context, double shrinkOffset, bool overlapsContent) {
        return Padding(
          padding: const EdgeInsets.all(20.0),
          child: Container(
            width: 150,
            height: 250,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(20),
              color: Colors.orange,
            ),
          ),
        );
      }
    
      @override
      // TODO: implement maxExtent
      double get maxExtent => 250;
    
      @override
      // TODO: implement minExtent
      double get minExtent => 250;
    
      @override
      bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
        return false;
      }
    }
    
    class AnimatedContainerDelegate extends SliverPersistentHeaderDelegate {
      final Animation<double> animation;
    
      AnimatedContainerDelegate({required this.animation});
    
      @override
      Widget build(
        BuildContext context,
        double shrinkOffset,
        bool overlapsContent,
      ) {
        return AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget? child) {
            final double paddingValue = 40.0 * (1 - animation.value);
            final EdgeInsets padding = EdgeInsets.all(paddingValue);
            final double borderRadiusValue = 20.0 * animation.value;
            final BorderRadius borderRadius = BorderRadius.only(
              bottomLeft: Radius.circular(borderRadiusValue),
              bottomRight: Radius.circular(borderRadiusValue),
            );
    
            return Container(
              margin: padding,
              decoration: BoxDecoration(
                color: Colors.yellow,
                borderRadius: borderRadius,
              ),
            );
          },
        );
      }
    
      @override
      double get maxExtent => 200;
    
      @override
      double get minExtent => 50;
    
      @override
      bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
        return true;
      }
    }

This video is output