Flutter下拉显示子头部并固定+下拉刷新功能实现问题
Flutter 下拉显示子头部及下拉刷新功能实现问题
我在Flutter中实现了下拉显示子头部的功能,但运行效果不佳,还需添加更高级特性:

需求细节:
- 子头部完全显示后固定在列表顶部,用户上滑一段距离后取消固定
- 用户继续下拉时,在子头部上方支持下拉刷新功能
我尝试了Stack+ListView的方案,但存在诸多问题:
- 子头部不属于滚动容器,导致:
- 执行下拉刷新时,子头部无法随列表项移动
- 用户无法通过在子头部上滑动来隐藏它,只能通过滚动列表实现
- 后续需要让AppBar中的红点通过动画转换为子头部,可能需要使用CustomScroll结合CustomPainter的方案,而非当前的Stack+ListView实现?
初始实现代码
class RevealSubheaderPage extends HookWidget { @override Widget build(BuildContext context) { const double HEIGHT = 96; final pinned = useState(false); final extend = useState(0.0); return Scaffold( appBar: AppBar(title: const Text('Test')), body: Stack( children: [ NotificationListener<ScrollNotification>( onNotification: (n) { if (pinned.value) { print(n.metrics.pixels); if (n.metrics.pixels > 0 && n.metrics.pixels < HEIGHT) { extend.value = n.metrics.pixels; } } else { if (n.metrics.pixels < 0) { extend.value = n.metrics.pixels; } if (n.metrics.pixels < -HEIGHT) { pinned.value = true; extend.value = -HEIGHT; } } return false; }, child: ListView.builder( itemCount: 42, itemBuilder: (context, index) { return Container( color: Colors.green, padding: const EdgeInsets.all(16), alignment: Alignment.centerLeft, child: Text('Item $index'), ); }, ), ), HideableWidget(height: HEIGHT, notifier: extend), ], ), ); } } class HideableWidget extends StatelessWidget { final double height; final ValueNotifier<double> notifier; HideableWidget({required this.height, required this.notifier}); @override Widget build(BuildContext context) { return ValueListenableBuilder<double>( valueListenable: notifier, builder: (context, value, child) { return Transform.translate( offset: Offset(0, -height - value), child: Container( height: height, width: double.infinity, color: Colors.red, alignment: Alignment.bottomCenter, child: Text("Subheader"), ), ); }, ); } }
更新:CustomScrollView实现尝试
我改用CustomScrollView实现了接近预期的效果,但如动图所示,子头部被移到了垂直居中位置。若要让它固定在顶部/底部,难以计算BouncingScrollPhysics的偏移量,无法让组件占据全部可用空间。虽然居中显示效果尚可,但我希望它能占满空间。此外,还未能集成RefreshIndicator,它应出现在子头部上方(需额外下拉100px的过滚动距离后渐显)。

更新后的实现代码
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; class Test3 extends StatelessWidget { const Test3({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: RevealSubheaderPage( revealHeight: 100, revealSnapHeight: 80, ), ), ); } } class RevealSubheaderPage extends HookWidget { final double revealHeight; final double revealSnapHeight; const RevealSubheaderPage({ super.key, required this.revealHeight, required this.revealSnapHeight, }); @override Widget build(BuildContext context) { final snap = useState(false); final scrollExtend = useState(0.0); final scrollController = useScrollController(); useEffect(() { l() { final o = scrollController.offset; scrollExtend.value = o; if (!snap.value && o < -revealSnapHeight) { snap.value = true; } else { if (snap.value && o > 0) { snap.value = false; } } } scrollController.addListener(l); return () => scrollController.removeListener(l); }); return CustomScrollView( controller: scrollController, slivers: [ const SliverAppBar( title: Text("Some Title"), pinned: true, ), SliverToBoxAdapter( child: Transform.translate( offset: Offset( 0, snap.value ? scrollExtend.value < 0 ? scrollExtend.value / 2 : 0 : scrollExtend.value < 0 ? scrollExtend.value / 2 : 0), child: AnimatedContainer( duration: const Duration(milliseconds: 150), height: snap.value ? revealHeight : scrollExtend.value < 0 ? -scrollExtend.value : 0, child: Container( height: revealHeight, width: double.infinity, color: Colors.red, alignment: Alignment.centerLeft, child: Text("Subheader"), ), ), ), ), SliverList( delegate: SliverChildBuilderDelegate( childCount: 42, (context, index) { return Container( color: Colors.green, padding: const EdgeInsets.all(16), alignment: Alignment.centerLeft, child: Text('Item $index'), ); }, ), ), ], ); } }
内容的提问来源于stack exchange,提问作者Stuck
相关产品推荐
相关产品推荐

