Multi headers are misbehaving
vanvixi opened this issue · 10 comments
After I changed your headerSliverBuilder to like below the error occurred:
-> Part of the text is lost
headerSliverBuilder: (context, innerScrolled) => <Widget>[
// use OverlapAbsorberPlus to wrap your SliverAppBar
const OverlapAbsorberPlus(
sliver: MySliverAppBar(),
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Text(
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.',
maxLines: 5,
textAlign: TextAlign.center,
overflow: TextOverflow.visible,
),
),
),
],
RPReplay_Final1699441144.MP4
Thank you for reporting the issue!
We have addressed it in the latest version, v1.0.1.
Please let me know if the problem has been resolved on your end.
@idootop
I'm sorry for reopening issues
If I have more than 1 pinned header, the body scroll height is not correct
It appears to be a separate issue. Could you please provide a simplified demo that reproduces the problem?
@idootop
When scrolling list, the other list will be scrolled by a distance equal to the height of the TabBar pinned
Code and demo video:
MySliverTabBarDelegate:
class MySliverTabBarDelegate extends SliverPersistentHeaderDelegate {
MySliverTabBarDelegate({required this.tabBar});
final TabBar tabBar;
@override
double get minExtent => tabBar.preferredSize.height;
@override
double get maxExtent => tabBar.preferredSize.height;
@override
bool shouldRebuild(MySliverTabBarDelegate oldDelegate) => false;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.black38,
height: tabBar.preferredSize.height,
child: tabBar,
);
}
}
Add to header:
headerSliverBuilder: (context, innerScrolled) => <Widget>[
// use OverlapAbsorberPlus to wrap your SliverAppBar
const OverlapAbsorberPlus(
sliver: MySliverAppBar(),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Text(
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.',
maxLines: 5,
textAlign: TextAlign.center,
overflow: TextOverflow.visible,
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: MySliverTabBarDelegate(
tabBar: TabBar(
labelColor: Colors.black,
unselectedLabelColor: Colors.black,
tabs: <Widget>[Text('Tab1.1'), Text('Tab2.2')],
),
),
),
],
Video:
RPReplay_Final1699518884.MOV
RPReplay_Final1699518884.2.MOV
Upon reviewing your layout, I noticed duplicate TabBar
widgets in the pinned header. Please clarify:
- The purpose of two
TabBar
widgets in the header. - The locations of the corresponding
TabView
widgets in your code. Sharing the entire layout code would be ideal. - Your intended final layout design.
Also, confirm that your headerSliverBuilder
contains a single SliverAppBar
. For multiple SliverAppBars
, you can use multiple NestedScrollViews
to link those headers and bodies.
- The above is just a sample demo, I only need to display the bottom TabBar.
- In my headerSliverBuilder there is SliverAppBar, SliverPadding, SliverPersistentHeader(TabBar)
Full code
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart';
void main() => runApp(
SafeArea(
top: true,
child: MaterialApp(
theme: ThemeData.light(useMaterial3: true).copyWith(
primaryColor: Colors.black,
tabBarTheme: const TabBarTheme(
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
),
appBarTheme: const AppBarTheme(backgroundColor: Colors.black),
),
home: const Example(),
),
),
);
class Example extends StatefulWidget {
const Example({super.key});
@override
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Widget _tabView([bool reverse = false]) => CustomScrollView(
key: PageStorageKey<String>('$reverse'),
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
slivers: <Widget>[
const OverlapInjectorPlus(),
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(_, index) => Container(
key: Key('$reverse-$index'),
color: index.isEven ? Colors.white : Colors.grey[100],
child: Center(
child: Text('ListTile ${reverse ? 30 - index : index + 1}'),
),
),
childCount: 30,
),
itemExtent: 60,
),
],
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollViewPlus(
// use key to access NestedScrollViewStatePlus
key: myKey,
headerSliverBuilder: (context, innerScrolled) => <Widget>[
// use OverlapAbsorberPlus to wrap your SliverAppBar
const OverlapAbsorberPlus(
sliver: MySliverAppBar(),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Text(
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.',
maxLines: 5,
textAlign: TextAlign.center,
overflow: TextOverflow.visible,
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: MySliverTabBarDelegate(
tabBar: TabBar(
labelColor: Colors.black,
unselectedLabelColor: Colors.black,
tabs: <Widget>[Text('Tab1.1'), Text('Tab2.2')],
),
),
),
],
body: TabBarView(
children: [
_tabView(),
_tabView(true),
],
),
),
),
);
}
final GlobalKey<NestedScrollViewStatePlus> myKey = GlobalKey();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// use GlobalKey<NestedScrollViewStatePlus> to access inner or outer scroll controller
myKey.currentState?.innerController.addListener(() {
final innerController = myKey.currentState!.innerController;
if (innerController.positions.length == 1) {
print('Scrolling inner nested scrollview: ${innerController.offset}');
}
});
myKey.currentState?.outerController.addListener(() {
final outerController = myKey.currentState!.outerController;
if (outerController.positions.length == 1) {
print('Scrolling outer nested scrollview: ${outerController.offset}');
}
});
});
}
}
class MySliverAppBar extends StatelessWidget {
///Header collapsed height
final minHeight = 60.0;
///Header expanded height
final maxHeight = 320.0;
const MySliverAppBar({super.key});
@override
Widget build(BuildContext context) {
final topPadding = MediaQuery.of(context).padding.top;
return SliverAppBar(
pinned: true,
stretch: true,
toolbarHeight: minHeight - topPadding,
collapsedHeight: minHeight - topPadding,
expandedHeight: maxHeight - topPadding,
titleSpacing: 0,
flexibleSpace: FlexibleSpaceBar(
stretchModes: const <StretchMode>[
StretchMode.zoomBackground,
StretchMode.blurBackground,
],
background: Image.network(
'https://pic1.zhimg.com/80/v2-fc35089cfe6c50f97324c98f963930c9_720w.jpg',
fit: BoxFit.cover,
alignment: const Alignment(0.0, 0.4),
),
),
);
}
}
class MySliverTabBarDelegate extends SliverPersistentHeaderDelegate {
MySliverTabBarDelegate({required this.tabBar});
final TabBar tabBar;
@override
double get minExtent => tabBar.preferredSize.height;
@override
double get maxExtent => tabBar.preferredSize.height;
@override
bool shouldRebuild(MySliverTabBarDelegate oldDelegate) => false;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.black38,
height: tabBar.preferredSize.height,
child: tabBar,
);
}
}
@vanvixi Unfortunately, the NestedScrollViewPlus
does not support the use of pinned
or floating
slivers in the headerSliverBuilder
, which is a known issue (flutter/flutter#79067) similar to the original NestedScrollView
.
Drawing from my experience, you can enhance the structure by moving the dynamic height SliverPadding and TabBar within the SliverAppBar. By pre-computing the dynamic height for SliverPadding and subsequently setting the appropriate height for the SliverAppBar, you can achieve an equivalent outcome.