如何让SliverPersistentHeaderDelegate先折叠再让其余内容滚动?
问题:实现SliverPersistentHeader先完全折叠再让内容滚动的效果
当前代码中,滚动时绿色头部栏与黄色内容区域会同时移动,需求是让绿色头部栏先完全折叠到最小高度,之后黄色内容才开始滚动到绿色栏下方。
当前代码
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), ), ), ), ), ); } }
当前滚动效果

期望效果
| 滚动前 | 滚动后 |
|---|---|
![]() | ![]() |
修改方案
核心是用NestedScrollView分离顶部固定栏与下方可折叠栏+内容,让下方的绿色栏优先完成折叠后,内容才开始滚动。具体修改如下:
- 将原
CustomScrollView替换为NestedScrollView,把顶部AppBar和间隔放在headerSliverBuilder中; - 把绿色栏、间隔和黄色内容放在
NestedScrollView的body内部的CustomScrollView中; - 调整滚动物理效果为
ClampingScrollPhysics,保证滚动连贯性; - 移除
SliverFillRemaining内部的多余SizedBox,避免高度冲突。
修改后的完整代码
import 'dart:math' as math; import 'package:flutter/material.dart'; class MyHomePage extends StatelessWidget { const MyHomePage({ super.key, }); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( physics: const ClampingScrollPhysics(), headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverPersistentHeader( pinned: true, delegate: SliverAppBarDelegate(), ), const SliverToBoxAdapter( child: SizedBox(height: 24), ), ], body: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: [ SliverPersistentHeader( pinned: true, delegate: SliverGreenMenuBarDelegate(), ), const SliverPadding(padding: EdgeInsets.only(bottom: 15)), SliverFillRemaining( 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), ), ), ), ), ); } }
内容的提问来源于stack exchange,提问作者felixrzayev
相关产品推荐
相关产品推荐



