Dynamic scrolling of NestedScrollView based on the content of TabBarView
TabBarView
requires finite height while wrapping with scrollable widget and on others cases all tabs become scrollable. Also trying with IndexedStack
provide the same behavior.
I am not using
TabBarView
.
I am loading widgets for tabs inside initState
and just passing inside body.
Run on dartPad.
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
final length = 8;
late final TabController controller;
final List<String> _tabs = <String>['Tab 1', 'Tab 2', 'Tab 3'];
List<Widget> tabViews = [];
@override
void initState() {
controller = TabController(
length: _tabs.length,
vsync: this,
)..addListener(() {
setState(() {});
});
tabViews = List.generate(
_tabs.length,
(index) => Column(
mainAxisSize: MainAxisSize.min,
children: [
...List.generate(
index * 3 + 2,
(itb) => Container(
alignment: Alignment.center,
height: 100,
width: double.infinity,
color: Color(Random().nextInt(0xffffffff)),
child: Text("Tab: $index item $itb"),
),
)
],
));
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
...List.generate(
5,
(index) => Container(
height: 50,
color: Colors.deepPurple,
width: double.infinity,
padding: const EdgeInsets.all(10),
child: Text(" top item $index"),
),
),
],
),
Container(
color: Colors.primaries.first,
height: kToolbarHeight,
child: TabBar(
tabs: _tabs.map((e) => Text(e)).toList(),
controller: controller,
),
),
tabViews[controller.index],
],
),
),
);
}
}
Comments
-
happy_san over 1 year
I've got a screen having some content on top of a
TabBar
.Both the content above
TabBar
and inTabBarView
can be of dynamic height. My use case is that the upper content should only be scrollable when all of the content is not visible and only up to the point that all of it becomes visible and not beyond that. So in the following example, onlyTab 1
should be scrollable.Setting the scrollphysics to
NeverScrollableScrollPhysics
wouldn't work since I can't determine the scroll behavior beforehand because of the dynamic height of the contents. UsingSliverAppBar
also doesn't work for the same reason.import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomePage(), ); } } class HomePage extends StatelessWidget { final length = 5; const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final List<String> _tabs = <String>['Tab 1', 'Tab 2', 'Tab 3']; return DefaultTabController( length: _tabs.length, child: Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return <Widget>[ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverToBoxAdapter( child: Column( children: [ Container( width: double.infinity, alignment: Alignment.center, child: Column( children: [ const Text('Upper Content'), ListView.builder( shrinkWrap: true, itemCount: length, itemBuilder: (_, __) => Container( padding: const EdgeInsets.all(5), alignment: Alignment.center, child: const Text('Items'), ), ) ], ), ), Container( color: Colors.blue, child: TabBar( tabs: _tabs .map( (String name) => Tab( text: name, ), ) .toList(), ), ) ], ), ), ), ]; }, body: TabBarView( children: _tabs.map((String name) { return name.split(' ')[1] != '3' ? SafeArea( top: false, bottom: false, child: Builder( builder: (BuildContext context) { return CustomScrollView( key: PageStorageKey<String>(name), slivers: <Widget>[ SliverOverlapInjector( handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), SliverPadding( padding: const EdgeInsets.all(8.0), sliver: SliverFixedExtentList( itemExtent: 48.0, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return ListTile( title: Text('Item $index'), ); }, childCount: name.split(' ')[1] != '2' ? 15 : 5, ), ), ), ], ); }, ), ) : Container( height: 50, width: 50, color: Colors.yellow, ); }).toList(), ), ), ), ); } }