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
Related
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,
),
),
],
),
),
);
}
}
I'm trying to implement a collapsable AppBar, using a FlexibleSpaberBar and Tabs, the code "works" fine with the content of one of the tabs is a ListView, but, when the content is more static (inside a Column) and is smaller than the actual screen size, then when I scroll up in order to hide the FlexibleSpacebar, the content if the tab gets behind the app bar.
As you can see, the content of Tab1Widget is smaller than the screen size (although depending on the device actual screen size, it could be bigger than the screen size) and when I scroll up in order to hide the FlexibleSpaceBar, the content of Tab1Widget gets behind the AppBar
Attaching a working example to reproduce the problem.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
elevation: 0.0,
title: const Text("App Bar Demo"),
expandedHeight: 200.0,
floating: true,
pinned: true,
forceElevated: innerBoxIsScrolled,
flexibleSpace: const FlexibleSpaceBar(
background: Placeholder(),
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(TabBar(
unselectedLabelColor: Colors.grey[700],
indicatorSize: TabBarIndicatorSize.label,
tabs: const [
Tab(
child: Align(
alignment: Alignment.center,
child: Text("Tab 1"),
),
),
Tab(
child: Align(
alignment: Alignment.center,
child: Text("Tab 2"),
),
),
])))
];
},
body: const TabBarView(
children: [
Tab1Widget(),
Tab2Widet(),
],
)),
),
);
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar);
final TabBar _tabBar;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
child: _tabBar,
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
class Tab1Widget extends StatelessWidget {
const Tab1Widget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Builder(builder: (BuildContext context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverSafeArea(
sliver: SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverToBoxAdapter(
child: Container(
color: Colors.green,
child: Column(
children: const [
Text(
"Content 1",
style: TextStyle(color: Colors.white),
),
Text(
"Content 2",
style: TextStyle(color: Colors.white),
),
Text(
"Content 3",
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
)
],
);
}),
);
}
}
class Tab2Widet extends StatelessWidget {
const Tab2Widet({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container();
}
}
Also, created a Dartpad here
The issue is on web when we try to scroll the vertically scrollable widget which is a child of horizontally scrollable widget where scroll bar is always visible.
When we scroll the horizontally scrollable widget the scroll bar for horizontal scroll is correctly displayed as shown in picture
horizontal scroll
But when we scroll the vertically scrollable widget the horizontal scrollbar disappears and new vertical scroll bar is seen at right as shown in screenshot
vertical scroll
Code to reproduce the issue.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scroll Issue Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Scroll Issue Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ScrollController _horizontalController = ScrollController();
final List<ScrollController> _verticalController = [];
#override
void initState() {
super.initState();
for (int i = 0; i < 6; i++) {
_verticalController.add(ScrollController());
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
children: <Widget>[
Expanded(
child: _buildHorizontalScroll(),
)
],
),
);
}
Widget _buildHorizontalScroll() {
return Scrollbar(
isAlwaysShown: true,
controller: _horizontalController,
notificationPredicate: (predicate) {
return predicate.depth == 0;
},
child: ListView.separated(
controller: _horizontalController,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Container(
margin: const EdgeInsets.all(30),
width: 300,
color: Colors.black12,
child: Column(
children: [
Expanded(
child: _buildVerticalScroll(index),
)
],
),
),
separatorBuilder: (context, index) => const Divider(
height: 30,
),
itemCount: 6),
);
}
Widget _buildVerticalScroll(int index) {
return Scrollbar(
scrollbarOrientation: ScrollbarOrientation.right,
isAlwaysShown: true,
controller: _verticalController[index],
child: ListView.separated(
controller: _verticalController[index],
itemBuilder: (context, index) => Container(
margin: const EdgeInsets.all(30),
height: 100,
color: Colors.green,
),
separatorBuilder: (context, index) => const Divider(
height: 30,
),
itemCount: 200),
);
}
}
Checking the notification predicate in the horizontal scroll bar, it detects the depth of scroll as both 0 and 1 when scrolling the vertically scrollable list. Is there a workaround for this problem? The problem started to appear since the upgrade to 2.5.0 for stable channel of flutter.
Maybe this package will help you easily customize your thumb and scrolling track both horizontally and vertically
https://pub.dev/packages/cross_scroll
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
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,
),
],
),
),
],
),
);
}
}