I'm running into a problem about SilverPersistentHeader obscuring SliverFillRemaining - flutter

First of all, my English is not good, sorry!
Here is my description of the problem:
consists of the following components: (contained in CustomScrollView)
1: SilverToBoxAdapter
2: SilverPersistentHeader
3: SliverFillRemaining
The SilverPersistentHeader only appears when the SilverToBoxAdapter disappears from the screen, and the SilverToBoxAdapter is hidden by default. After the SilverPersistentHeader appears, it will have the effect of pined=true as the page slides.
When SilverPersistentHeader appeared, I found that SilverPersistentHeader would block part of SliverFillRemaining.
here is my code:
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 MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late ScrollController _mainController;
late PageController _pageController;
late bool isScrollable = true;
bool isVisiable = false;
#override
void initState() {
super.initState();
_mainController = ScrollController();
_pageController = PageController();
_pageController.addListener(() {
debugPrint('offset:${_pageController.page}');
if (_pageController.page == 0.0) {
setState(() {
isScrollable = true;
});
} else {
setState(() {
isScrollable = false;
});
}
});
_mainController.addListener(() {
// debugPrint('offset:${_mainController.offset}');
if (_mainController.offset >= 200) {
setState(() {
isVisiable = true;
});
} else {
setState(() {
isVisiable = false;
});
}
});
}
#override
void dispose() {
super.dispose();
_mainController.dispose();
_pageController.dispose();
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
physics: isScrollable
? const AlwaysScrollableScrollPhysics()
: const NeverScrollableScrollPhysics(),
controller: _mainController,
slivers: <Widget>[
const SliverAppBar(
title: Text('SliverDemo'),
pinned: true,
),
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.amber,
),
),
SliverPersistentHeader(
delegate: MyPersistentHeader(
isVisiable: isVisiable,
child: Container(
color: Colors.green,
)),
pinned: true,
),
SliverFillRemaining(
hasScrollBody: true,
child: PageView(
controller: _pageController,
children: [
Container(
color: Colors.primaries[0],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[1],
child: Column(
children: [
Container(
color: Colors.black,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[2],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[3],
child: Column(
children: [
Container(
color: Colors.black,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[4],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
)
],
),
),
],
);
}
}
class MyPersistentHeader extends SliverPersistentHeaderDelegate {
final Widget child;
final bool isVisiable;
MyPersistentHeader({required this.child, required this.isVisiable});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
#override
double get maxExtent => isVisiable ? 60 : 0;
#override
double get minExtent => isVisiable ? 60 : 0;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
I've searched online for a long time and can't find a solution. I hope you can give me a good suggestion, thanks.

Related

How to implement dynamic scrolling via a navbar in flutter for a single-page web app?

I'm trying to find a way to click on an element in my navbar and have the page auto-scroll down to that section. My one-page website consists of a SingleChildScrollView with the different sections (e.g. about, services, contact us,..) as children of that scroll view. This structure is written in my HomeScreen class. The NavBar is in a different dart file and gets generated. How can I make it so that these get linked to each other. GIF: https://imgur.com/a/pukCfao
Homescreen class (gets initiated in main.dart)
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
TopSection(),
SizedBox(height: kDefaultPadding * 2),
AboutSection(),
ServiceSection(),
TestimonialSection(),
SizedBox(height: kDefaultPadding),
ContactSection(),
SizedBox(height: 50)
],
),
),
);
}
}
Part where you can click on element in nav bar
class _MenuState extends State<Menu> {
int selectedIndex = 0;
int hoverIndex = 0;
List<String> menuItems = [
"Home",
"About",
"Services",
"Testimonials",
"Contact"
];
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: kDefaultPadding * 2.5),
constraints: BoxConstraints(maxWidth: 1110),
height: 100,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
boxShadow: [kDefaultShadow],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
menuItems.length,
(index) => buildMenuItem(index),
),
),
);
}
Widget buildMenuItem(int index) => InkWell(
onTap: () {
setState(() {
selectedIndex = index;
});
},
Example of navbar with underneath the sections
UPDATE:
I solved the problem with a little change in your code and work for me :)
I use ValueNotifier and ValueLisenableBuilder for transfer value & use ValueChanged for pass value to outside the menu widget.
but better and recommended using one state manager for this work .
for switch scroll can using GlobalKey and Scrollable.ensureVisible.
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: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late ValueNotifier<int> notifier;
final List<GlobalKey> itemKeys = [
GlobalKey(),
GlobalKey(),
GlobalKey(),
GlobalKey(),
GlobalKey()
];
#override
void initState() {
notifier = ValueNotifier(0);
super.initState();
}
#override
void dispose() {
notifier.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: ValueListenableBuilder<int>(
valueListenable: notifier,
builder: (context, val, widget) {
return Column(children: [
Menu(
valueChanged: (int value) {
print("$value");
notifier.value = value;
notifier.notifyListeners();
Scrollable.ensureVisible(itemKeys[value].currentContext!,
duration: Duration(seconds: 1),
// duration for scrolling time
alignment: .5,
// 0 mean, scroll to the top, 0.5 mean, half
curve: Curves.easeInOutCubic);
},
),
Item(title: "Home", key: itemKeys[0]),
Item(title: "About", key: itemKeys[1]),
Item(title: "Services", key: itemKeys[2]),
Item(title: "Testimonials", key: itemKeys[3]),
Item(title: "Contact", key: itemKeys[4]),
]);
},
)),
);
}
}
class Item extends StatelessWidget {
const Item({Key? key, required this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context) {
return Container(
height: 400,
width: double.infinity,
color: Colors.deepPurpleAccent,
child: Text("$title"),
);
}
}
class Menu extends StatefulWidget {
Menu({Key? key, required this.valueChanged}) : super(key: key);
ValueChanged<int> valueChanged;
#override
State<Menu> createState() => _MenuState();
}
class _MenuState extends State<Menu> {
int selectedIndex = 0;
int hoverIndex = 0;
List<String> menuItems = [
"Home",
"About",
"Services",
"Testimonials",
"Contact"
];
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 70 * 2.5),
constraints: BoxConstraints(maxWidth: 1110),
height: 100,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
menuItems.length,
(index) => Container(
color: selectedIndex == index ? Colors.red : Colors.green,
width: 100,
height: 100,
child: InkWell(
child: Text("${menuItems[index]}"),
onTap: () {
setState(() {
selectedIndex = index;
widget.valueChanged((index));
});
},
),
),
),
),
);
}
}

Synchronizing ListWheelScrollView in Flutter

When I scroll through one List Wheel Scroll View, the other list either lags or does not scroll smoothly.
https://pub.dev/packages/linked_scroll_controller allows to sync lists but does not support FixedExtendScrollPhysics.
Output : -
https://pub.dev/packages/linked_scroll_controller works perfectly if we are using ScrollPhysics but throws an error when used with a widget that uses FixedExtendScrollPhysics. I want both the list to move Synchronizing that is if I move green list I want red list to move simultaneously and vice versa
Code :
import 'dart:math';
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: 'List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const List(),
);
}
}
class List extends StatefulWidget {
const List({Key? key}) : super(key: key);
#override
_ListState createState() => _ListState();
}
class _ListState extends State<List> {
final scrollController = FixedExtentScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("List"),
backgroundColor: Colors.green,
),
body: Row(
children: [
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
itemExtent: 100,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (value) {
setState(() {
scrollController.animateToItem(value,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut);
});
},
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.green,
height: 50,
width: 50,
)
]
]),
),
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
controller: scrollController,
physics: const FixedExtentScrollPhysics(),
itemExtent: 100,
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.red,
height: 50,
width: 50,
)
]
]),
)
],
));
}
}
Really interesting question. The problem was syncing both the scrollviews. I made few changes to your code to achieve the desired result.
The basic idea is to remove listener to the other scroll before forcing pixels. After the scroll, add the same listener. But because it happens instantaneously and actual scroll happens sometimes in future, they don't overlap perfectly.
So I had to introduce CancelableCompleter from the async library to make sure add operation does not happen if another scroll event had happened.
With forcePixels, the scrolling to other wheel is not deferred hence CancelableCompleter is not required.
// ignore_for_file: invalid_use_of_protected_member
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: 'List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const List(),
);
}
}
class List extends StatefulWidget {
const List({Key? key}) : super(key: key);
#override
_ListState createState() => _ListState();
}
class _ListState extends State<List> {
final _firstScrollController = FixedExtentScrollController();
final _secondScrollController = FixedExtentScrollController();
#override
void initState() {
super.initState();
_firstScrollController.addListener(_firstScrollListener);
_secondScrollController.addListener(_secondScrollListener);
}
#override
void dispose() {
_firstScrollController
..removeListener(_firstScrollListener)
..dispose();
_secondScrollController
..removeListener(_secondScrollListener)
..dispose();
super.dispose();
}
void _firstScrollListener() {
_secondScrollController.removeListener(_secondScrollListener);
_secondScrollController.position.forcePixels(_firstScrollController.offset);
_secondScrollController.addListener(_secondScrollListener);
}
void _secondScrollListener() {
_firstScrollController.removeListener(_firstScrollListener);
_firstScrollController.position.forcePixels(_secondScrollController.offset);
_firstScrollController.addListener(_firstScrollListener);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("List"),
backgroundColor: Colors.green,
),
body: Row(
children: [
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
itemExtent: 100,
controller: _firstScrollController,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (value) {
print('first wheel : item selected: $value');
},
children: [
for (int i = 0; i < 25; i++) ...[
Container(
color: Colors.green,
height: 50,
width: 50,
)
]
]),
),
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
controller: _secondScrollController,
physics: const FixedExtentScrollPhysics(),
itemExtent: 100,
onSelectedItemChanged: (value) {
print('second wheel : item selected: $value');
},
children: [
for (int i = 0; i < 25; i++) ...[
Container(
color: Colors.red,
height: 50,
width: 50,
)
]
]),
)
],
));
}
}
I am using protective member function forcePixels as Flutter has not provided any way to set pixels without animation without creating subclass of ScrollPosition. If you are fine with this linter warning, it is all good. If not, we will have to extend ListWheelScrollView to use ScrollPosition where we could make changes as per need.
Try separate two controller and add listener like this:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const List(),
);
}
}
class List extends StatefulWidget {
const List({Key? key}) : super(key: key);
#override
_ListState createState() => _ListState();
}
class _ListState extends State<List> {
final scrollController1 = FixedExtentScrollController();
final scrollController2 = FixedExtentScrollController();
#override
void initState() {
super.initState();
scrollController1.addListener(() {
if (scrollController1.position.hasPixels) {
scrollController2.animateTo(
scrollController1.offset,
duration: const Duration(milliseconds: 10), //adjust delay you need
curve: Curves.easeInOut,
);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("List"),
backgroundColor: Colors.green,
),
body: Row(
children: [
SizedBox(
height: 600,
width: 150,
child: ListWheelScrollView(
itemExtent: 100,
controller: scrollController1,
physics: const FixedExtentScrollPhysics(),
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.green,
height: 50,
width: 50,
)
]
]),
),
SizedBox(
height: 600,
width: 150,
child: ListWheelScrollView(
controller: scrollController2,
physics: const FixedExtentScrollPhysics(),
itemExtent: 100,
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.red,
height: 50,
width: 50,
)
]
]),
)
],
));
}
}
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
MyWidget({Key? key}) : super(key: key);
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late ScrollController _controllerA;
late ScrollController _controllerB;
/// FixedExtentScrollController
/// late FixedExtentScrollController _controllerA;
/// late FixedExtentScrollController _controllerB;
#override
void initState() {
super.initState();
_controllerA = ScrollController();
_controllerB = ScrollController();
/// _controllerA = FixedExtentScrollController();
/// _controllerB = FixedExtentScrollController();
_controllerA.addListener(() {
if (_controllerA.position.hasPixels) {
_controllerB.jumpTo(_controllerA.offset);
}
});
/// if you neet bind _controllerB to _controllerA
/// _controllerB.addListener(() {
/// if (_controllerB.position.hasPixels) {
/// _controllerA.jumpTo(_controllerB.offset);
/// }
///});
}
#override
void dispose() {
_controllerA.dispose();
_controllerB.dispose();
super.dispose();
}
Widget _listView(Color color, ScrollController controller) {
var width = MediaQuery.of(context).size.width / 2;
return Container(
width: width,
child: ListView.builder(
controller: controller,
shrinkWrap: true,
itemCount: 100,
itemExtent: 50,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: color, border: Border.all(color: Colors.white)),
);
}),
);
}
/// ListWheelScrollView
/// Widget _listView(Color color, FixedExtentScrollController controller) {
/// var width = MediaQuery.of(context).size.width / 2;
/// return Container(
/// width: width,
/// child: ListWheelScrollView(
/// physics: FixedExtentScrollPhysics(),
/// controller: controller,
/// itemExtent: 50,
/// children: [
/// ...List.generate(
/// 100,
/// (index) => Container(
/// decoration: BoxDecoration(
/// color: color,
/// border: Border.all(color: Colors.white)),
/// ))
/// ]),
/// );
/// }
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Row(
children: [
_listView(Colors.red, _controllerA),
_listView(Colors.yellow, _controllerB),
],
)),
);
}
}

How to go back to previous screen by clicking on bottom navigation bar item in Flutter

I am using this library persistent_bottom_nav_bar to display bottom navigation bar even on navigating to new screen. Now there are two main pages Page1 and Page2, Page1 is using an icon of home where as Page2 is using an icon of search. In Page1 contain a button which navigate to new screen named as NewPage. What i wanted to achieve is if i navigate to NewPage from Page1 and if i decide to goback to previous screen which is Page1 by clicking on homeicon which is at bottom. So how can i click on bottom item and go back to previous screen? Hope you understand my question
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
home: HomeScaffold(),
);
}
}
class HomeScaffold extends StatefulWidget {
#override
_HomeScaffoldState createState() => _HomeScaffoldState();
}
class _HomeScaffoldState extends State<HomeScaffold> {
late PersistentTabController _controller;
#override
void initState() {
super.initState();
_controller = PersistentTabController(initialIndex: 0);
}
List<Widget> _buildScreens() {
return [
Page1(),
Page2(),
];
}
List<PersistentBottomNavBarItem> _navBarsItems() {
return [
_buildBottomNavBarItem('Page 1', Icons.home),
_buildBottomNavBarItem('Page 2', Icons.search),
];
}
#override
Widget build(BuildContext context) {
return PersistentTabView.custom(
context,
controller: _controller,
screens: _buildScreens(),
confineInSafeArea: true,
itemCount: 2,
handleAndroidBackButtonPress: true,
stateManagement: true,
screenTransitionAnimation: ScreenTransitionAnimation(
animateTabTransition: true,
curve: Curves.ease,
duration: Duration(milliseconds: 200),
),
customWidget: CustomNavBarWidget(
items: _navBarsItems(),
onItemSelected: (index) {
setState(() {
_controller.index = index; // go back to previous screen if i navigate to new screen
});
},
selectedIndex: _controller.index,
),
// ),
);
}
}
class CustomNavBarWidget extends StatelessWidget {
final int? selectedIndex;
final List<PersistentBottomNavBarItem> items;
final ValueChanged<int>? onItemSelected;
CustomNavBarWidget({
Key? key,
this.selectedIndex,
required this.items,
this.onItemSelected,
});
Widget _buildItem(PersistentBottomNavBarItem item, bool isSelected) {
return Container(
alignment: Alignment.center,
height: kBottomNavigationBarHeight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: IconTheme(
data: IconThemeData(
size: 26.0,
color: isSelected
? (item.activeColorSecondary == null
? item.activeColorPrimary
: item.activeColorSecondary)
: item.inactiveColorPrimary == null
? item.activeColorPrimary
: item.inactiveColorPrimary),
child: isSelected ? item.icon : item.inactiveIcon ?? item.icon,
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Material(
type: MaterialType.transparency,
child: FittedBox(
child: Text(
item.title!,
style: TextStyle(
color: isSelected
? (item.activeColorSecondary == null
? item.activeColorPrimary
: item.activeColorSecondary)
: item.inactiveColorPrimary,
fontWeight: FontWeight.w400,
fontSize: 12.0),
)),
),
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Container(
width: double.infinity,
height: kBottomNavigationBarHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items.map((item) {
int index = items.indexOf(item);
return Flexible(
child: GestureDetector(
onTap: () {
this.onItemSelected!(index);
},
child: _buildItem(item, selectedIndex == index),
),
);
}).toList(),
),
),
);
}
}
PersistentBottomNavBarItem _buildBottomNavBarItem(String title, IconData icon) {
return PersistentBottomNavBarItem(
icon: Icon(icon),
title: title,
activeColorPrimary: Colors.indigo,
inactiveColorPrimary: Colors.grey,
);
}
class Page1 extends StatefulWidget {
const Page1({Key? key}) : super(key: key);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Container(
child: Center(
child: TextButton(
onPressed: () {
Navigator.push(
context, CupertinoPageRoute(builder: (context) => NewPage()));
},
child: Text('Click'),
),
),
),
);
}
}
class Page2 extends StatefulWidget {
const Page2({Key? key}) : super(key: key);
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.red,
body: Container(),
);
}
}
class NewPage extends StatefulWidget {
const NewPage({Key? key}) : super(key: key);
#override
_NewPageState createState() => _NewPageState();
}
class _NewPageState extends State<NewPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
);
}
}
Have you tried simply using the built-in Navigator.of(context).pop() function as the onPressed callback?

Flutter web tabbar scroll issue with non primary scrollcontroller

In continuation with question
The solution provided above is good. But hard for me to implement in my project.
Expected results:
I've created two tabs.
In each tab I have SingleChildScrollView wrapped with Scrollbar.
I can not have the primary scrollcontroller in both the tabs, because that throws me exception: "ScrollController attached to multiple scroll views."
For Tab ONE I use primary scrollcontroller, for Tab TWO I created Scrollcontroller and attached it.
Widgets in both the tabs should be scrollabale using keyboard and mouse.
Actual results:
For Tab ONE with primary scrollcontroller I can scroll both by keyboard and dragging scrollbar.
But for Tab TWO with non primary scrollcontroller, I have to scroll only by dragging scrollbar. This tab doesn't respond to keyboard page up /down keys.
When keyboard keys are used in Tab TWO actually contents of tab ONE are getting scrolled.
Check code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TabExample(),
);
}
}
class TabExample extends StatefulWidget {
const TabExample({Key key}) : super(key: key);
#override
_TabExampleState createState() => _TabExampleState();
}
class _TabExampleState extends State<TabExample> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Text('Tab ONE')),
Tab(icon: Text('Tab TWO')),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
WidgetC(),
WidgetD(),
],
),
),
);
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.blue,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
ScrollController _scrollController;
#override
void initState() {
_scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.green,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
controller: _scrollController,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
This has been accepted as a bug in flutter.
Pl follow for progress here: https://github.com/flutter/flutter/issues/83711
Note for other developers facing same issue.
To overcome the mentioned problem, I changed my design layout. Instead of tabbar view I used Navigationrail widget. This solved my problem.
NavigationRail widget allowed me to attach primary scroll controller to multiple widgets without giving me exception: "ScrollController attached to multiple scroll views."
Sample code.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
WidgetC _widgetC = WidgetC();
WidgetD _widgetD = WidgetD();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('NavigationRail Demo'), centerTitle: true),
body: Row(
children: <Widget>[
NavigationRail(
elevation: 8.0,
selectedIndex: _selectedIndex,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.all,
groupAlignment: 0.0,
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Tab ONE'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('Tab TWO'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: _getPageAtIndex(_selectedIndex),
)
],
),
);
}
Widget _getPageAtIndex(int index) {
switch (index) {
case 0:
return _widgetC;
case 1:
return _widgetD;
}
return Container();
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
// ScrollController _scrollController;
#override
void initState() {
// _scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
// _scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
// controller: _scrollController,
child: SingleChildScrollView(
// controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}

How to have the drawer push the content instead of going on top of it?

I'm trying to build something similar to the slack app (see screenshot below) where the navigation drawer pushes the screen away instead of going on top.
I've been trying with the Drawer component without success. I've also looked at PageView but it seems that the children need to take 100% of the width.
Does someone have an idea of how to implement it?
EDIT
A similar result can be achieved with a Stack and AnimatedPositioned
class SlidingDrawer extends StatefulWidget {
final Widget drawer;
final Widget child;
final int swipeSensitivity;
final double drawerRatio;
final Color overlayColor;
final double overlayOpacity;
final int animationDuration;
final Curve animationCurve;
SlidingDrawer({
Key key,
#required this.drawer,
#required this.child,
this.swipeSensitivity = 25,
this.drawerRatio = 0.8,
this.overlayColor = Colors.black,
this.overlayOpacity = 0.5,
this.animationDuration = 500,
this.animationCurve = Curves.ease,
}) : super(key: key);
#override
_SlidingDrawerState createState() => _SlidingDrawerState();
}
class _SlidingDrawerState extends State<SlidingDrawer> {
bool _opened = false;
void open() {
setState(() {
_opened = true;
});
}
void close() {
setState(() {
_opened = false;
});
}
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
final drawerWidth = width * widget.drawerRatio;
return GestureDetector(
onHorizontalDragUpdate: (details) {
if (details.delta.dx > widget.swipeSensitivity) {
open();
} else if (details.delta.dx < -widget.swipeSensitivity) {
close();
}
},
child: SizedBox(
width: width,
height: height,
child: Stack(
children: [
AnimatedPositioned(
width: drawerWidth,
height: height,
left: _opened ? 0 : -drawerWidth,
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.animationCurve,
child: Container(
color: Colors.amber,
child: widget.drawer,
),
),
AnimatedPositioned(
height: height,
width: width,
left: _opened ? drawerWidth : 0,
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.animationCurve,
child: Stack(
fit: StackFit.expand,
children: [
widget.child,
AnimatedSwitcher(
duration: Duration(milliseconds: widget.animationDuration),
switchInCurve: widget.animationCurve,
switchOutCurve: widget.animationCurve,
child: _opened
? GestureDetector(
onTap: () {
setState(() {
_opened = false;
});
},
child: Container(
color: widget.overlayColor.withOpacity(
widget.overlayOpacity,
),
),
)
: null,
)
],
),
),
],
),
),
);
}
}
ORIGINAL ANSWER
As pointed out by #Yadu in the comment
you could use Single child horizontal scroll view (with disabled scroll physics) with Scrollable.ensureVisible(context) to show the menu
using an horizontal scroll view is working.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _drawerOpened = false;
final drawerKey = new GlobalKey();
final mainKey = new GlobalKey();
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
children: [
Container(
key: drawerKey,
color: Colors.green,
width: MediaQuery.of(context).size.width * 0.8,
),
SizedBox(
key: mainKey,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Scaffold(
appBar: AppBar(
title: Text("My Page"),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleDrawer,
),
),
body: Container(
color: Colors.yellow,
width: MediaQuery.of(context).size.width,
),
),
),
],
),
);
}
void _toggleDrawer() {
setState(() {
_drawerOpened = !_drawerOpened;
});
if (_drawerOpened) {
Scrollable.ensureVisible(drawerKey.currentContext);
} else {
Scrollable.ensureVisible(mainKey.currentContext);
}
}
}