'Page level scrolling per TabBarView with widgets on top
I would like to implement the following behavior in Flutter for web where scrollbars allow to scroll all the way up and down of the page on any of the tabs. Scrolling on the ListView should take us to the top of the page and not only to the top of the list.
Code
class TestPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.black12,
child: const Center(child: Text('Sliver 1')),
),
),
const SliverToBoxAdapter(
child: Center(
child: TabBar(
labelColor: Colors.black,
tabs: [
Tab(text: 'Sliver Tab 1'),
Tab(text: 'Sliver Tab 2'),
],
),
)),
];
},
body: TabBarView(
children: [
ListView.builder(
// controller: ScrollController(),
itemCount: 1000,
itemBuilder: (context, index) => Container(
height: 50,
color: index.isEven ? Colors.red : Colors.white,
child: AliveKeeper(index: index),
)),
ListView.builder(
// controller: ScrollController(),
itemCount: 1000,
itemBuilder: (context, index) => Container(
height: 50,
color: index.isEven ? Colors.blue : Colors.white,
child: AliveKeeper(index: index),
),
)
],
),
),
),
);
}
}
class AliveKeeper extends StatefulWidget {
final int index;
const AliveKeeper({
required this.index,
GlobalKey? key,
}) : super(key: key);
@override
AliveKeeperState createState() => AliveKeeperState();
}
class AliveKeeperState extends State<AliveKeeper>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Text('Item ${widget.index}');
}
}
As you can see here, I've tried different implementations using NestedScrollView but I haven't got the expected behavior.
Keep in mind that each TarBarView must be configured with keep alive.
Thanks.
Solution 1:[1]
Try this code I think helps this
and mouse hovers example :
https://protocoderspoint.com/hover-effect-with-animation-flutter-web/
this code only GridView example
class TestPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.black12,
child: const Center(child: Text('Sliver 1')),
),
),
const SliverToBoxAdapter(
child: Center(
child: TabBar(
labelColor: Colors.black,
tabs: [
Tab(text: 'Sliver Tab 1'),
Tab(text: 'Sliver Tab 2'),
],
),
)),
];
},
body: TabBarView(
children: [
GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
crossAxisCount: 3,
crossAxisSpacing: 4.0,
mainAxisSpacing: 4.0
),
itemCount: 1000,
itemBuilder: (context, index) => Container(
height: 50,
color: index.isEven ? Colors.red : Colors.white,
child: AliveKeeper(index: index),
)),
GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
crossAxisCount: 3,
crossAxisSpacing: 4.0,
mainAxisSpacing: 4.0
),
itemCount: 1000,
itemBuilder: (context, index) => Container(
height: 50,
color: index.isEven ? Colors.red : Colors.white,
child: AliveKeeper(index: index),
)),
],
),
),
),
);
}
}
class AliveKeeper extends StatefulWidget {
final int index;
const AliveKeeper({
required this.index,
GlobalKey? key,
}) : super(key: key);
@override
AliveKeeperState createState() => AliveKeeperState();
}
class AliveKeeperState extends State<AliveKeeper>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Text('Item ${widget.index}');
}
}
Solution 2:[2]
ListView.builder(
primary = false;
......
......
),
Might help you with your issue.
Solution 3:[3]
here I have tried to solve using CustomScrollController.
import 'dart:developer';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'demo/navigation.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: 'Custom Clipper',
theme: ThemeData(
brightness: Brightness.light,
// add tabBarTheme
tabBarTheme: TabBarTheme(
labelColor: Colors.pink[800],
labelStyle: TextStyle(color: Colors.pink[800]), // color for text
indicator: const UnderlineTabIndicator(
// color for indicator (underline)
borderSide: BorderSide(color: Colors.black12))),
primaryColor: Colors.pink[800], // outdated and has no effect to Tabbar
// deprecated,
),
// initialRoute: NavigationUtils.routeHome,
// onGenerateRoute: NavigationUtils.generateRoute,
home: const UsersList(),
);
}
}
class UsersList extends StatefulWidget {
final String? searchString;
const UsersList({Key? key, this.searchString}) : super(key: key);
@override
_UsersListState createState() => _UsersListState();
}
class _UsersListState extends State<UsersList>
with SingleTickerProviderStateMixin {
//for display search bar
bool isShowSearchBar = false;
//creating dynamic a list for side popup menu
List<PopupMenuItem> popUpMenuItem = [];
//Scroll controller
final ScrollController _scrollController = ScrollController();
//TabBar controller
late TabController tbController;
@override
void initState() {
//initializing with 4 tab items
tbController = TabController(length: 4, vsync: this);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
scrollBehavior: MyCustomScrollBehavior(),
physics: const ClampingScrollPhysics(),
slivers: [
SliverAppBar(
title: searchBar(),
backgroundColor: isShowSearchBar ? Colors.white : null,
elevation: 0,
pinned: true,
floating: true,
),
SliverAppBar(
toolbarHeight: 200,
title: Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Row(children: [
const Padding(
padding: EdgeInsets.all(10.0),
child: CircleAvatar(
radius: 90,
),
),
Expanded(
child: Container(
height: 200,
margin: const EdgeInsets.only(left: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"Name: Rohit Chaurasiya",
style: TextStyle(color: Colors.black),
),
Text("posts: 13", style: TextStyle(color: Colors.black)),
]),
))
]),
),
backgroundColor: Colors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize:
Size(MediaQuery.of(context).size.width, kToolbarHeight),
child: Stack(
alignment: Alignment.topCenter,
children: [
const Divider(
thickness: 1,
color: Colors.black12,
height: 15,
),
TabBar(
controller: tbController,
isScrollable: true,
indicatorColor: Colors.black,
indicatorWeight: 5,
indicatorSize: TabBarIndicatorSize.label,
padding: const EdgeInsets.symmetric(horizontal: 10),
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(color: Colors.black, width: 3.0),
insets: EdgeInsets.only(bottom: 45)),
tabs: const [
Icon(Icons.camera_alt_outlined),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Tab(
text: "CHATS",
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Tab(text: "STATUS"),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Tab(text: "CALLS"),
)
]),
],
),
),
),
SliverFillRemaining(
child: TabBarView(
controller: tbController,
children: [
ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Text("Camera $index");
},
),
ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Text("Chat $index");
},
),
const Center(
child: Text("STATUS"),
),
const Center(
child: Text("CALLS"),
),
],
),
)
],
shrinkWrap: true,
));
}
Widget searchBar() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
(isShowSearchBar)
? Expanded(
child: TextField(
decoration: const InputDecoration(
hintText: "Search...",
prefixIcon: Icon(
Icons.search,
color: Colors.grey,
)),
onChanged: (searchQuery) {
setState(() {
searchQuery = searchQuery.toLowerCase();
});
},
),
)
: const Text("MyChat"),
Row(
children: [
InkWell(
onTap: () {
setState(() {
isShowSearchBar = !isShowSearchBar;
});
},
child: Icon(
isShowSearchBar ? Icons.close : Icons.search,
color: isShowSearchBar ? Colors.grey : Colors.white,
)),
PopupMenuButton(
icon: const Icon(FontAwesomeIcons.ellipsisV),
onSelected: _selectedMenu,
itemBuilder: (context) => popUpMenuItem)
],
)
],
);
}
void _selectedMenu(value) {
log("Selected menu => " + value.toString());
}
}
class UserListTileWidget extends StatelessWidget {
const UserListTileWidget({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
try {} on PlatformException catch (e) {
log("sdfcsfg");
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
child: Row(
children: <Widget>[
Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
),
],
),
child: const CircleAvatar(
radius: 25,
backgroundImage: AssetImage("assets/images/contact.jpeg"),
),
),
Container(
width: MediaQuery.of(context).size.width * 0.65,
padding: const EdgeInsets.only(
left: 20,
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: const <Widget>[
Text(
"Rohit",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
/*Container(
margin: const EdgeInsets.only(left: 5),
width: 7,
height: 7,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.orange,
),
)*/
],
),
],
),
],
),
),
],
),
),
);
}
}
class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like dragDevices
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
// etc.
};
}
Solution 4:[4]
TabBarView
uses PageView
and it uses children max height for TabBarView
's children. While your goal is to keep alive, you can use SingleChildScrollView
, this answer main goal is to control the scroll-bar as per tab.
Run on dartPad.
class _TestPageState extends State<TestPage>
with SingleTickerProviderStateMixin {
final int tabLength = 3;
late final controller = TabController(length: tabLength, vsync: this)
..addListener(() {
setState(() {});
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: Column(
children: [
Container(
height: 200,
color: Colors.black12,
child: const Center(child: Text('Sliver 1')),
),
TabBar(
controller: controller,
labelColor: Colors.black,
tabs: List.generate(
tabLength,
(i) => Tab(text: 'Sliver Tab ${i + 1}'),
),
),
GestureDetector(
onTap: () {},
onHorizontalDragEnd: (details) {
if (details.primaryVelocity == null) return;
debugPrint(details.primaryVelocity.toString());
if (details.primaryVelocity! > 0) {
debugPrint("move Left");
controller.animateTo(
controller.index < 0 ? 0 : controller.index - 1);
} else if (details.primaryVelocity! < 0) {
debugPrint("move right");
controller.animateTo(controller.index >= tabLength
? tabLength - 1
: controller.index + 1);
}
},
child:
/// or any animation builder you prefer
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: [
tab1(),
tab2(),
tab3(),
].elementAt(controller.index),
),
),
],
),
),
),
);
}
Column tab2() {
return Column(
children: List.generate(
22,
(index) => Container(
height: 50,
width: double.infinity,
color: index.isEven ? Colors.blue : Colors.white,
child: AliveKeeper(index: index),
),
),
);
}
Column tab3() {
return Column(
children: List.generate(
22,
(index) => Container(
height: 50,
width: double.infinity,
color: index.isEven ? Colors.pink : Colors.white,
child: AliveKeeper(index: index),
),
),
);
}
Column tab1() {
return Column(
children: List.generate(
32,
(index) => Container(
width: double.infinity,
height: 50,
color: index.isEven ? Colors.red : Colors.white,
child: AliveKeeper(index: index),
)),
);
}
}
Solution 5:[5]
You can use multiple ScrollController
s for various scrollable Widget
s that you have, and coordinate/synchronize their ScrollPosition
s by listening to changes in their ScrollController
s.
Checkout this DartPad. I have added a third tab in DartPad which changes the size dynamically.
static void controllerListener(
ScrollController parent,
ScrollController child,
ScrollController sc,
bool movedScrollbar,
) {
final childPX = child.position.pixels;
final parentPX = parent.position.pixels;
final parentPXMax = parent.position.maxScrollExtent;
if (childPX >= 0 && parentPX < parentPXMax) {
parent.position.moveTo(childPX + parentPX);
} else {
final currenParentPos = childPX + parentPX;
parent.position.moveTo(currenParentPos);
}
if (!movedScrollbar) {
sc.position.moveTo(parentPX + childPX);
}
}
@override
this function scrolls header widgets according to changes in ListView
widget.
Also you wanted a page level Scrollbar, therefore I made a SingleChildScrollView
spanning the whole page and according Scrollbar
position changes in SingleChildScrollView
widget I changed the main Widget
. If you think the calculation for scroll position is off you can change the scrollController.addListener
code starting from line 199
in DartPad.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Anmol Mishra |
Solution 2 | aedemirsen |
Solution 3 | |
Solution 4 | |
Solution 5 |