How to prevent Hero animation from covering SliverAppBar? - flutter

I have a Hero animation that should animate an image from a ListView to the background of a FlexibleSpaceBar inside of a SliverAppBar.
This works all fine, except that the hero animation takes place on top of the SliverAppBar and when the animation is finished, the SliverAppBarsuddenly pops in on top of the background, which doesn't look so nice.
How can I either a) include the SliverAppBar in the hero transition so that it fades in on top of the image as the transition progresses or b) make the hero transition take place under the SliverAppBar without covering it?
Sample Code:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/details': (context) => DetailsPage(),
},
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: AppBar().preferredSize,
child: Hero(
tag: 'appbar',
child: AppBar(
title: Text('title'),
actions: <Widget>[
PopupMenuButton<String>(
itemBuilder: (BuildContext context) {
return [
PopupMenuItem<String>(
child: Text('Action'),
)
];
},
),
],
),
),
),
body: ListView.builder(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
itemCount: 10,
itemBuilder: (context, index) => buildListItem(context, index),
),
);
}
Widget buildListItem(BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: () async {
await Navigator.of(context).pushNamed(
'/details',
arguments: index,
);
},
child: Row(
children: <Widget>[
Hero(
tag: index,
child:
SizedBox(width: 80, height: 80, child: widgetForIndex(index)),
),
],
),
),
);
}
}
Widget widgetForIndex(int index) {
return Container(color: Color.fromARGB(255, max(0, 255 - 20 * index), 0, 0));
}
class DetailsPage extends StatefulWidget {
#override
_DetailsPageState createState() => _DetailsPageState();
}
class _DetailsPageState extends State<DetailsPage> {
#override
Widget build(BuildContext context) {
final int index = ModalRoute.of(context).settings.arguments;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text('Details'),
expandedHeight: 300,
flexibleSpace: FlexibleSpaceBar(
background: Hero(
tag: index,
child: widgetForIndex(index),
),
),
),
SliverToBoxAdapter(
child: Column(
children: [
Container(
color: Colors.green,
width: 200,
height: 200,
),
],
),
),
],
),
);
}
}

Related

Flutter use SliverAppBar with Hero widget

I'm having trouble using the Hero widget with SliverAppBar.
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: Center(
child: Row(
children: [Colors.red, Colors.blue, Colors.yellow]
.map((e) => GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ScrollPage(color: e),
));
},
child: Hero(
tag: e,
child: Container(
width: 100,
height: 100,
color: e,
),
),
))
.toList(),
),
),
);
}
}
import 'package:flutter/material.dart';
class ScrollPage extends StatefulWidget {
const ScrollPage({super.key, required this.color});
final Color color;
#override
State<ScrollPage> createState() => _ScrollPageState();
}
class _ScrollPageState extends State<ScrollPage> {
// final GlobalKey key = GlobalKey<SliverState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
Hero(
tag: widget.color,
child: SliverAppBar.large(
key: UniqueKey(),
expandedHeight: 200,
backgroundColor: widget.color,
title: const Text(
'This is a title blblaa',
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item $index'),
),
childCount: 100,
),
),
],
),
);
}
}
I'm getting error like:
════════ Exception caught by widgets library ═══════════════════════════════════
'package:flutter/src/widgets/framework.dart': Failed assertion: line 6405 pos 12: 'renderObject.child == child': is not true.
and
════════ Exception caught by widgets library ═══════════════════════════════════
'package:flutter/src/widgets/framework.dart': Failed assertion: line 6369 pos 12: 'child == _child': is not true.
framework.dart:6369
The relevant error-causing widget was
MaterialApp
and sometimes:
════════ Exception caught by widgets library ═══════════════════════════════════
Duplicate GlobalKey detected in widget tree.
════════════════════════════════════════════════════════════════════════════════
I did similar not using Appbar and I achieved it. Demo video here. But, I really want to use SliverAppbar in this case.
Full minimal reproducible project: https://github.com/iqfareez/flutter_hero_sliver
How do I make the SliverAppBar work with Hero?
The issue is here Hero is a general widget rather than a sliver-widget. That's the issue occurs while wrapping the SliverAppBar with Hero widget.
You can do
SliverToBoxAdapter(
child: Hero(
tag: widget.color,
child: // customAppBar but it might loss the scroll-effect,
Also, you can wrap the Scaffold with Hero widget, but it will show a little different animation.
You can create SliverPersistentHeaderDelegate.
Check the pr and commit difference.
import 'package:flutter/material.dart';
class ScrollPage extends StatefulWidget {
const ScrollPage({super.key, required this.color});
final Color color;
#override
State<ScrollPage> createState() => _ScrollPageState();
}
class _ScrollPageState extends State<ScrollPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
delegate: MySliverPersistentHeaderDelegate(widget.color)),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item $index'),
),
childCount: 100,
),
),
],
),
);
}
}
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final tag;
MySliverPersistentHeaderDelegate(this.tag);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Hero(
tag: tag,
child: Material(
color: tag,
child: Stack(
children: [
Align(
child: const Text(
'This is a title blblaa',
),
),
Align(
alignment: Alignment.topLeft,
child: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: Icon(Icons.arrow_back)),
),
],
),
),
);
}
#override
double get maxExtent => 200;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}
You can try moving the Hero widget outside of the SliverAppBar, wrapping it around the entire CustomScrollView. Here's how you can modify your code to achieve that:
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Hero(
tag: 'row',
child: Center(
child: Row(
children: [Colors.red, Colors.blue, Colors.yellow]
.map((e) => GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ScrollPage(color: e),
));
},
child: Container(
width: 100,
height: 100,
color: e,
),
))
.toList(),
),
),
),
);
}
}
class ScrollPage extends StatefulWidget {
const ScrollPage({super.key, required this.color});
final Color color;
#override
State<ScrollPage> createState() => _ScrollPageState();
}
class _ScrollPageState extends State<ScrollPage> {
// final GlobalKey key = GlobalKey<SliverState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Hero(
tag: 'row',
child: CustomScrollView(
slivers: [
SliverAppBar(
key: UniqueKey(),
expandedHeight: 200,
backgroundColor: widget.color,
title: const Text(
'This is a title blblaa',
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item $index'),
),
childCount: 100,
),
),
],
),
),
);
}
}

Creating a global floating button in flutter

Is it possible to have floating button visible throughout the life cycle of the app on top of all pages? I know that with Scaffold I can have it but it only works for that page and i'll lose it once I push a new page on the navigator stack.
Yes, OverlayEntry is made for this purpose.
To insert it, you can do something like this:
ElevatedButton(
child: Text("Overlay Test"),
onPressed: () {
final entry = OverlayEntry(
builder: (context) => Container(
color: Colors.blue,
),
);
Overlay.of(context)?.insert(entry);
},
)
If you want to remove it later, you can save the entry variable and then call entry.remove() when needed.
Full example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
OverlayEntry? _entry;
double _left = 50;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("OverlayEntry Demo"),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
child: Text("Add OverlayEntry"),
onPressed: () {
_entry = OverlayEntry(
builder: (context) {
print("build");
return Positioned(
left: _left,
top: 200,
child: Container(
width: 150,
height: 150,
color: Colors.grey,
),
);
},
);
Overlay.of(context)?.insert(_entry!);
},
),
ElevatedButton(
child: Text("Move it"),
onPressed: () {
_left += 10;
_entry?.markNeedsBuild();
},
),
ElevatedButton(
child: Text("Remove it"),
onPressed: () => _entry?.remove(),
),
],
),
),
);
}
}
One option is to use the builder of the MaterialApp to create a Stack with your Button on top:
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test',
home: TestPage(),
initialRoute: "/test",
builder: (context, child) {
return Scaffold(
body: Stack(
children: [
child!,
Positioned(
left: 0,
bottom: 0,
child: **your button here**,
),
],
),
);
},
routes: routes(context),
);
}
}

Navigator blocks communication between CustomScrollView and NestedScrollView

Expected behaviour:
The nested scroll view scrolls normaly if the custom scroll view gets scrolled and thus scrolls the appbar with it.
Actually happening:
Only the custom scrollview scrolls, but not the appbar (=nested scrollview).
Wanted Behaviour can be achieved by either not using a customscrollview or not using a navigator (both no option in my case). This implementation worked before null safety if it helps in any case.
Code example:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TestScreen(),
);
}
}
class TestScreen extends StatefulWidget {
#override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> {
GlobalKey<NavigatorState> get navigatorKey =>
GlobalKey<NavigatorState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text("title"),
pinned: true,
floating: false,
snap: false,
expandedHeight: 200,
))
],
// body: DemoPage(),
body: Navigator(
key: navigatorKey,
onGenerateRoute: (settings) =>
MaterialPageRoute(builder: (context) => DemoPage())),
));
}
}
class DemoPage extends StatelessWidget {
Color generateRandomColor1() {
// Define all colors you want here
const predefinedColors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.black,
Colors.white
];
Random random = Random();
return predefinedColors[random.nextInt(predefinedColors.length)];
}
#override
Widget build(BuildContext context) {
// return Container(
// color: Colors.red,
// height: 1000,
// width: 500,
// child: Center(
// child: RaisedButton(
// child: Text("Press"),
// onPressed: () {
// Navigator.of(context)
// .push(MaterialPageRoute(builder: (context) => DemoPage()));
// },
// ),
// ),
// );
return CustomScrollView(slivers: [
SliverToBoxAdapter(
child: Container(
color: generateRandomColor1(),
height: 1000,
width: 500,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RaisedButton(
child: Text("Press"),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => DemoPage()));
},
),
RaisedButton(
child: Text("pop"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
),
)
]);
}
}
Probably, your NestedScrollView acts as if there is an individual scroll view when DemoPage builds the CustomScrollView below the Navigator widget.
You can set the physics of the CustomScrollView to NeverScrollableScrollPhysics, so that the NestedScrollView can control it.
...
return CustomScrollView(
// Add the following line:
physics: NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Container(
color: generateRandomColor1(),
height: 1000,
width: 500,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RaisedButton(
child: Text("Press"),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DemoPage(),
),
);
},
),
RaisedButton(
child: Text("pop"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
),
),
],
);
...
PageView
If making CustomScrollView never scrollable doesn't give the desired result, you can try PageView instead of Navigator. Since you want to keep SliverAppBar stationary across all pages, this will provide navigation only inside PageView.
All you need to do is to put the list of your pages in PageView and pass its controller to your pages. Then, you can navigate to other pages via a button or swipe gesture.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TestScreen(),
);
}
}
class TestScreen extends StatefulWidget {
#override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> {
GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
// Page controller
final PageController controller = PageController(initialPage: 0);
return Scaffold(
body: NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text("title"),
pinned: true,
floating: false,
snap: false,
expandedHeight: 200,
),
)
],
// Page view here
body: PageView(
// Scroll horizontally
scrollDirection: Axis.horizontal,
controller: controller,
// Your pages
children: <Widget>[
// Pass the controller to your page
DemoPage(pageController: controller),
DemoPage(pageController: controller),
DemoPage(pageController: controller),
],
),
),
);
}
}
class DemoPage extends StatelessWidget {
// Page Controller
final PageController pageController;
DemoPage({required this.pageController});
Color generateRandomColor1() {
// Define all colors you want here
const predefinedColors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.black,
Colors.white
];
Random random = Random();
return predefinedColors[random.nextInt(predefinedColors.length)];
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
color: generateRandomColor1(),
height: 1000,
width: 500,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RaisedButton(
child: Text("Press"),
onPressed: () {
// Go to next page
pageController.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.linear,
);
},
),
RaisedButton(
child: Text("pop"),
onPressed: () {
// Go back to previous page
pageController.previousPage(
duration: Duration(milliseconds: 300),
curve: Curves.linear,
);
},
),
],
),
),
),
)
],
);
}
}
It should work I guess.
Also have a look at this post ref: nested_reference
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
pinned: true,
floating: true,
forceElevated: innerBoxIsScrolled,
.....
You can find the answer here:
Answer from github
There are 2 solutions:
First, get innerController of NestedScrollView and pass it by the PrimaryScrollController widget below Navigator
Second, access the state of NestedScrollView from your widget where you need a controller of NestedScrollView, get innerController of NestedScrollViewState and assign that innerController to your scrollable widget
P.s.
You can pass controller by InheritedWidget
First-way example:
...
GlobalKey<NavigatorState> get navigatorKey =>
GlobalKey<NavigatorState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text("title"),
pinned: true,
floating: false,
snap: false,
expandedHeight: 200,
))
],
// body: DemoPage(),
body: Builder(builder: (context) {
var scrollController = PrimaryScrollController.of(context);
return Navigator(
key: navigatorKey,
onGenerateRoute: (settings) =>
MaterialPageRoute(builder: (context) => PrimaryScrollController(controller: scrollController!,
child: Scaffold(body:ListView(
children:[
/// children
],
),
),
))),
);
}
);
}
}
The second way:
You can innerController of NestedScrollView
context.findAncestorStateOfType<NestedScrollViewState>()?.innerController
then pass it to your scrollable widget
CustomScrollView(
controller: context.findAncestorStateOfType<NestedScrollViewState>()?.innerController,
...
)
I think that Navigator does not pass innerController of NestedView
Try removing pinned property of SliverAppBar.
Flutter documentation has interactive example of what it does.
https://api.flutter.dev/flutter/material/SliverAppBar-class.html

CustomScrollView within ExpansionTile error

Tried to fix this reading some documentation and some open issues but was not lucky.. Could someone please help?
I am getting this error:
type 'bool' is not a subtype of type 'double' in type cast
Not sure why though I tried adding container wrapping the component, adding height, adding flexible box etc...
No lucky
`import 'package:flutter/material.dart';
class SampleData {
SampleData(this.title, [this.children = const <SizedBox>[]]);
final String title;
final List<SizedBox> children;
}
final List<SampleData> data = <SampleData>[
SampleData("IT", [
SizedBox(
height: 300,
width: 300,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
new SliverToBoxAdapter(
child: Text('fesfefes'),
),
],
),
),
]),
];
class Branch extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test 123'),
),
body: Container(
width: 500,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) => Item(data[index]),
itemCount: data.length,
),
),
);
}
}
// Displays one Entry. If the entry has children then it's displayed
// with an ExpansionTile.
class Item extends StatelessWidget {
const Item(this.sample);
final SampleData sample;
Widget _buildTiles(SampleData root) {
return SizedBox(
width: 500,
child: ExpansionTile(
key: PageStorageKey<SampleData>(root),
title: Text(root.title),
children: root.children,
),
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(sample);
}
}
`
You can copy paste run full code blow
You can remove
//key: PageStorageKey<SampleData>(root),
working demo
full code
import 'package:flutter/material.dart';
class SampleData {
SampleData(this.title, [this.children = const <SizedBox>[]]);
final String title;
final List<SizedBox> children;
}
final List<SampleData> data = <SampleData>[
SampleData("IT", [
SizedBox(
height: 300,
width: 300,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
new SliverToBoxAdapter(
child: Text('fesfefes'),
),
],
),
),
]),
];
class Branch extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test 123'),
),
body: Container(
width: 500,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) => Item(data[index]),
itemCount: data.length,
),
),
);
}
}
// Displays one Entry. If the entry has children then it's displayed
// with an ExpansionTile.
class Item extends StatelessWidget {
const Item(this.sample);
final SampleData sample;
Widget _buildTiles(SampleData root) {
return SizedBox(
width: 500,
child: ExpansionTile(
//key: PageStorageKey<SampleData>(root),
title: Text(root.title),
children: root.children,
),
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(sample);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Branch(),
);
}
}

SliverAppBar() not collapsing with ListView in Flutter

I'm trying to display tabs for each main tab (Nested Tab Bar) in SliverAppBar(). It's look like this:
See the image
See the GIF
The content of the exam tab it's in Container() widget (That the error in the image came from).
Now, with the Container() widget the SliverAppBar() will collapse when the user scroll the exam tab content (white screen in the image), everything is fine for now.
So, After I replaced the Container() with ListView.builder() to make the tab content scrollable, now I can't collapse SliverAppBar() from the tab content (white screen in the image). but I can from the SliverAppBar().
See this GIF after I added ListView.builder()
So, How I can make the SliverAppBar scrollable (collapsing ) with Listview?
Can anyone help me? please :(
This example (demo):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'SliverAppBar App Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: SliverSafeArea(
top: false,
sliver: SliverAppBar(
pinned: true,
title: Text(widget.title),
expandedHeight: 500,
),
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(tabs: [Tab(text: 'Tab A'), Tab(text: 'Tab B')]),
Colors.blue,
),
pinned: false,
),
];
},
body: TabBarView(
children: <Widget>[
NestedTabs('A'),
NestedTabs('B'),
],
),
),
),
),
);
}
}
// This class is to handle the main tabs (Tab A & Tab B)
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar, this._color);
TabBar _tabBar;
final Color _color;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: _color,
alignment: Alignment.center,
child: _tabBar,
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
class NestedTabs extends StatelessWidget {
final String mainTabName;
NestedTabs(this.mainTabName);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(20),
child: Container(
color: Colors.blue,
alignment: Alignment.bottomCenter,
child: TabBar(
tabs: [
Tab(text: 'Tab $mainTabName-1'),
Tab(text: 'Tab $mainTabName-2')
],
),
),
),
body: TabBarView(
children: [
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: 500,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
width: 200,
color: Colors.black45,
child: Center(child: Text('Index ${index}')));
},
),
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: 500,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
width: 200,
color: Colors.black45,
child: Center(child: Text('Index ${index}')));
},
)
],
),
);
}
}
Thank you :)
Use SliverList() instead of SliverFillRemaining for ListView