Related
I have a list of videos urls/youtube IDs and a PageView.builder. I want to load next/previous video according to position in the list. For example:
List videos = ['vid_0', 'vid_1', 'vid_2', 'vid_3', 'vid_4']
if current Playing video is vid_2, it should reload the next video or previous video of the list based on button event.
Currently when I press next or previous button, the same video is loaded but the PageView makes pages equal to the length of list. I am using youtube_video_player package.
Here is my code:
import 'package:flutter/material.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
void main(){
runApp(WatchVideoLesson());
}
class WatchVideoLesson extends StatefulWidget {
const WatchVideoLesson({Key? key}) : super(key: key);
#override
State<WatchVideoLesson> createState() => _WatchVideoLessonState();
}
class _WatchVideoLessonState extends State<WatchVideoLesson> {
List ytIDs = ['mluJOYd17L8', 'd-RCKfVjFI4', 'xXPuaB7UpB0'];
var playingVideo;
final _pageController = PageController();
late YoutubePlayerController _controller;
bool _isPlayerReady = false;
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void initState() {
_controller = YoutubePlayerController(
initialVideoId: ytIDs.first,
flags: const YoutubePlayerFlags(
autoPlay: false,
)
);
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text('Week'),
backgroundColor: Colors.red,
),
body:
Column(
children:[
Expanded(
child: PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
itemCount: ytIDs.length,
itemBuilder: (context, index) {
return Column(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.grey,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
onPressed: () {
_pageController.previousPage(duration: Duration(seconds: 1), curve: Curves.ease);
},
child: Icon(Icons.arrow_back_ios_new,)),
SizedBox(width: 10,),
Flexible(child: Text('Video Title', overflow: TextOverflow.ellipsis,)),
SizedBox(width: 10,),
ElevatedButton(style: ElevatedButton.styleFrom(
backgroundColor: Colors.green),
onPressed: () {
_pageController.nextPage(duration: Duration(seconds: 1), curve: Curves.easeInOut);
},
child: Icon(Icons.arrow_forward_ios,)),
],
),
),
SizedBox(height: 20,),
YoutubePlayer(
progressColors: const ProgressBarColors(
playedColor: Colors.red,
handleColor: Colors.green),
controller: _controller,
showVideoProgressIndicator: true,
onReady: (){
_isPlayerReady = true;
},
),
SizedBox(height: 10,),
],
);
}
),
),
]
),
),
);
}
}
Current snippet using the same controller on YoutubePlayer and my guess this is issue having same video. I separating the widget so that it can have different controller for PageView item.
class WatchVideoLesson extends StatefulWidget {
const WatchVideoLesson({Key? key}) : super(key: key);
#override
State<WatchVideoLesson> createState() => _WatchVideoLessonState();
}
class _WatchVideoLessonState extends State<WatchVideoLesson> {
List ytIDs = ['mluJOYd17L8', 'd-RCKfVjFI4', 'xXPuaB7UpB0'];
var playingVideo;
final _pageController = PageController();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(children: [
Expanded(
child: PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
itemCount: ytIDs.length,
itemBuilder: (context, index) {
return Column(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.grey,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
onPressed: () {
_pageController.previousPage(
duration: Duration(seconds: 1),
curve: Curves.ease);
},
child: Icon(
Icons.arrow_back_ios_new,
)),
SizedBox(
width: 10,
),
Flexible(
child: Text(
'Video Title',
overflow: TextOverflow.ellipsis,
)),
SizedBox(
width: 10,
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green),
onPressed: () {
_pageController.nextPage(
duration: Duration(seconds: 1),
curve: Curves.easeInOut);
},
child: Icon(
Icons.arrow_forward_ios,
)),
],
),
),
YTPlayer(ytIDs: ytIDs[index]),
SizedBox(
height: 20,
),
SizedBox(
height: 10,
),
],
);
}),
),
]),
),
);
}
}
class YTPlayer extends StatefulWidget {
final String ytIDs;
const YTPlayer({super.key, required this.ytIDs});
#override
State<YTPlayer> createState() => _YTPlayerState();
}
class _YTPlayerState extends State<YTPlayer> {
late YoutubePlayerController _controller;
bool _isPlayerReady = false;
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_controller = YoutubePlayerController(
initialVideoId: widget.ytIDs,
flags: const YoutubePlayerFlags(
autoPlay: false,
));
}
#override
Widget build(BuildContext context) {
return YoutubePlayer(
controller: _controller,
progressColors: const ProgressBarColors(
playedColor: Colors.red, handleColor: Colors.green),
showVideoProgressIndicator: true,
onReady: () {
_isPlayerReady = true;
},
);
}
}
This is a TabBar which animates when the index is selected. Is there a way to make the long text LOOOOT that is animating not over-lapping the TabBar, or just animate behind the TabBar's layout?
TabTest
class TabTest extends StatefulWidget {
#override
_TabTestState createState() => _TabTestState();
}
class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
late TabController _tabController;
late List<AnimationController> _animationControllers;
#override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
_animationControllers = List.generate(
4,
(i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
reverseDuration: Duration(milliseconds: 350),
));
}
#override
Widget build(BuildContext context) {
Widget _tab(IconData iconData, String text) {
const _tabTextStyle = TextStyle(
fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
return SizedBox(
height: 50,
child: Tab(
icon: Icon(iconData, color: Colors.black),
child: Text(text, style: _tabTextStyle),
),
);
}
List<Widget> _tabs = [
_tab(Icons.card_giftcard, 'LOOOOOOOTTTT'),
_tab(Icons.confirmation_num_outlined, 'Voucher'),
_tab(Icons.emoji_events_outlined, 'Testing'),
_tab(Icons.wine_bar_outlined, 'Testing'),
];
List<Widget> _animationGenerator() {
return List.generate(
_tabs.length,
(index) => AnimatedBuilder(
animation: _animationControllers[index],
builder: (ctx, child) {
final child = _tabs[index];
final value = _animationControllers[index].value;
final angle = math.sin(value * math.pi * 2) * math.pi * 0.08;
print(angle);
return Transform.rotate(angle: angle, child: child);
}),
);
}
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(100),
child: AppBar(
iconTheme: Theme.of(context).iconTheme,
title: Text(
'Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size.fromHeight(20),
child: Container(
child: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.black,
tabs: _animationGenerator(),
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.white,
spreadRadius: 5.0,
offset: Offset(0, 3))
],
),
),
),
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(
4,
(index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
void _listener() {
if (_tabController.indexIsChanging) {
_animationControllers[_tabController.previousIndex].reverse();
} else {
_animationControllers[_tabController.index].forward();
}
}
#override
void dispose() {
super.dispose();
_tabController.removeListener(_listener);
}
}
This is what I have. No package used. Planning to animate the Container with bottom border of the selected Tab. (currently using SizedBox to wrap the Tab) and animate it behind the TabBar rather than over-lapping the TabBar.
EDIT: Include code
Wrap the tab widget with a clipRect.
I want to implement listview builder inside ExpansionPanelList. Under each expansion tile it will have tabbar and tabbarview. Inside the tabbarview it will display listview. I have tried to implemented but the view is not rendering properly. Throwing error
RenderBox was not laid out: RenderRepaintBoundary#92515 NEEDS-LAYOUT NEEDS-PAINT
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1930 pos 12: 'hasSize'
My Implemented code:
List<bool> _isOpen =[true, false]
ExpansionPanelList(
children: [
ExpansionPanel(
isExpanded: _isOpen[0],
headerBuilder: (context, isOpen){
return Text("Activities");
},
body: Column(
children: [
Container(child: tabView()),
Flexible(child: tabBarView()),
],
),),
ExpansionPanel(
isExpanded: _isOpen[1],
headerBuilder: (context, isOpen){
return Text("Ports");
},
body: Text("Test"),)
],
expansionCallback: (i, isOpen) {
setState(() {
_isOpen[i] =!isOpen;
});
},
),
tabview widget
Widget tabView(){
return PreferredSize(
preferredSize: Size.fromHeight(70),
child: Align(
alignment: Alignment.centerLeft,
child: Container(
width: MediaQuery.of(context).size.width ,
child: TabBar(
isScrollable: true,
labelPadding: EdgeInsets.symmetric(horizontal: 8),
controller: _tabController,
unselectedLabelColor: CustomColors.Black,
indicatorSize: TabBarIndicatorSize.label,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: CustomColors.oldLace),
tabs: tabNames),
),
));
}
tabbarView
Widget tabBarView(){
return TabBarView(controller: _tabController,
children: [
listItems(),
Center(child: Text("Favorite list will be shown here")),
Center(child: Text("Filter")),
],);
}
Wrap your ExpansionPanelList in SingleChildScrollView and Give fix height to your TabBarView by Wraping Inside Container.
Full Code Snippet below
import 'package:flutter/material.dart';
class Expand extends StatefulWidget {
const Expand({Key? key}) : super(key: key);
#override
_ExpandState createState() => _ExpandState();
}
class _ExpandState extends State<Expand> with SingleTickerProviderStateMixin {
List<bool> _isOpen = [false, false];
late TabController _tabController;
static const List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: myTabs.length);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
Widget tabView() {
return PreferredSize(
preferredSize: Size.fromHeight(70),
child: Align(
alignment: Alignment.centerLeft,
child: Container(
width: MediaQuery.of(context).size.width,
// height: 200,
child: TabBar(
isScrollable: true,
labelPadding: EdgeInsets.symmetric(horizontal: 8),
controller: _tabController,
unselectedLabelColor: Colors.black,
indicatorSize: TabBarIndicatorSize.label,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(50),
// color: CustomColors.oldLace
),
tabs: myTabs,
// tabs: tabNames
),
),
));
}
Widget tabBarView() {
return Container(
height: 400,
width: 400,
child: TabBarView(
controller: _tabController,
children: [
// listItems(),
Center(child: Text("Favorite list will be shown here")),
Center(child: Text("Filter")),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
// height: 400,
// width: 500,
child: ExpansionPanelList(
children: [
ExpansionPanel(
isExpanded: _isOpen[0],
headerBuilder: (context, isOpen) {
return Text("Activities");
},
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
tabView(),
tabBarView(),
],
),
),
ExpansionPanel(
isExpanded: _isOpen[1],
headerBuilder: (context, isOpen) {
return Text("Ports");
},
body: Text("Test"),
)
],
expansionCallback: (i, isOpen) {
setState(() {
_isOpen[i] = !isOpen;
});
},
),
),
);
}
}
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Expansion Panel List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
List<bool> _isOpen = [true, false];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Draggable Test'),
),
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
ExpansionPanelList(
children: [
ExpansionPanel(
isExpanded: _isOpen[0],
headerBuilder: (context, isOpen) {
return Text("Activities");
},
body: Column(
children: [
Container(child: tabView()),
tabBarView(),
],
)),
ExpansionPanel(
isExpanded: _isOpen[1],
headerBuilder: (context, isOpen) {
return Text("Ports");
},
body: Column(
children: [
Container(child: tabView()),
tabBarView(),
],
))
],
expansionCallback: (i, isOpen) {
setState(() {
_isOpen[i] = !isOpen;
});
},
),
],
));
}
Widget tabView() {
return PreferredSize(
preferredSize: Size.fromHeight(70),
child: Align(
alignment: Alignment.centerLeft,
child: Container(
width: MediaQuery.of(context).size.width,
child: TabBar(
isScrollable: true,
labelPadding: EdgeInsets.symmetric(horizontal: 8),
controller: _tabController,
unselectedLabelColor: Colors.blue,
indicatorSize: TabBarIndicatorSize.label,
indicator: BoxDecoration(borderRadius: BorderRadius.circular(50), color: Colors.black),
tabs: [
Tab(text: 'Activities'),
Tab(text: 'Ports'),
],
),
),
));
}
Widget tabBarView() {
return Container(
height: 100,
child: TabBarView(
controller: _tabController,
children: [
// listItems(),
Center(child: Text("Favorite list will be shown here")),
Center(child: Text("Filter")),
],
),
);
}
}
I'm getting a problem with my TabBar I want it to be scrollable (swipe left and right by gesture).
But it's not working, I have to click on the sub category name to view it but I want it to scroll right to go to the next sub category.
I set isScrollable: true, in the TabBar widget but still the same problem.
If there is any suggestion I'm here thank you :)
1st screen
2nd screen
class CategoryScreen extends StatefulWidget {
final CategoryModel categoryModel;
CategoryScreen({#required this.categoryModel});
#override
_CategoryScreenState createState() => _CategoryScreenState();
}
class _CategoryScreenState extends State<CategoryScreen>
with TickerProviderStateMixin {
int _tabIndex = 0;
#override
void initState() {
super.initState();
Provider.of<CategoryProvider>(context, listen: false)
.getSubCategoryList(context, widget.categoryModel.id.toString());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: ResponsiveHelper.isDesktop(context)
? PreferredSize(
child: MainAppBar(), preferredSize: Size.fromHeight(80))
: null,
body: Consumer<CategoryProvider>(
builder: (context, category, child) {
return category.subCategoryList != null
? Center(
child: Scrollbar(
child: Container(
width: 1170,
child: CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: [
SliverAppBar(
expandedHeight: 200,
toolbarHeight:
50 + MediaQuery.of(context).padding.top,
pinned: true,
floating: false,
backgroundColor: Theme.of(context).primaryColor,
leading: IconButton(
icon: Icon(Icons.chevron_left,
color: ColorResources.COLOR_WHITE),
onPressed: () => Navigator.pop(context)),
flexibleSpace: FlexibleSpaceBar(
title: Text(widget.categoryModel.name,
style: rubikMedium.copyWith(
fontSize: Dimensions.FONT_SIZE_LARGE,
color: Colors.white)),
titlePadding: EdgeInsets.only(
bottom: 54 +
(MediaQuery.of(context).padding.top / 2),
left: 50,
right: 50,
),
background: Container(
margin: EdgeInsets.only(bottom: 50),
child: FadeInImage.assetNetwork(
placeholder: Images.placeholder_rectangle,
image:
'${Provider.of<SplashProvider>(context, listen: false).baseUrls.categoryImageUrl}/${widget.categoryModel.image}',
fit: BoxFit.cover,
),
),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(30.0),
child: Container(
width: MediaQuery.of(context).size.width,
color: Theme.of(context).accentColor,
**child: TabBar(
controller: TabController(
initialIndex: _tabIndex,
length:
category.subCategoryList.length + 1,
vsync: this),
isScrollable: true,
unselectedLabelColor:
ColorResources.getGreyColor(context),
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.label,
indicatorColor:
Theme.of(context).primaryColor,
labelColor: Theme.of(context)
.textTheme
.bodyText1
.color,
tabs: _tabs(category),
onTap: (int index) {
_tabIndex = index;
if (index == 0) {
category.getCategoryProductList(context,
widget.categoryModel.id.toString());
} else {
category.getCategoryProductList(
context,
category.subCategoryList[index - 1].id
.toString());
}
},
),
),
),
),**
SliverToBoxAdapter(
child: category.categoryProductList != null
? category.categoryProductList.length > 0
? GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 5,
mainAxisSpacing: 5,
childAspectRatio: 4,
crossAxisCount: ResponsiveHelper
.isDesktop(context)
? 3
: ResponsiveHelper.isTab(
context)
? 2
: 1),
itemCount:
category.categoryProductList.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(
Dimensions.PADDING_SIZE_SMALL),
itemBuilder: (context, index) {
return ProductWidget(
product: category
.categoryProductList[index]);
},
)
: NoDataScreen()
: GridView.builder(
shrinkWrap: true,
itemCount: 10,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(
Dimensions.PADDING_SIZE_SMALL),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 5,
mainAxisSpacing: 5,
childAspectRatio: 4,
crossAxisCount:
ResponsiveHelper.isDesktop(context)
? 3
: ResponsiveHelper.isTab(context)
? 2
: 1,
),
itemBuilder: (context, index) {
return ProductShimmer(
isEnabled:
category.categoryProductList ==
null);
},
),
),
],
),
),
),
)
: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor)));
},
),
);
}
List<Tab> _tabs(CategoryProvider category) {
List<Tab> tabList = [];
tabList.add(Tab(text: 'Tout'));
category.subCategoryList
.forEach((subCategory) => tabList.add(Tab(text: subCategory.name)));
return tabList;
}
}
You need to use a NestedScrollView to use a Sliver with a TabBar
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: SliverTabExample(),
),
);
}
class SliverTabExample extends StatefulWidget {
const SliverTabExample({Key? key}) : super(key: key);
#override
_SliverTabExampleState createState() => _SliverTabExampleState();
}
class _SliverTabExampleState extends State<SliverTabExample>
with SingleTickerProviderStateMixin {
final _tabs = List.generate(10, (index) => 'Tab#${index + 1}');
late final TabController _tabCont;
#override
void initState() {
_tabCont = TabController(length: 10, vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (_, __) {
return [
SliverAppBar(
expandedHeight: 200,
pinned: true,
floating: false,
title: Text('TabBar Example'),
centerTitle: true,
bottom: TabBar(
controller: _tabCont,
isScrollable: true,
tabs: [
..._tabs.map(
(label) => Tab(
child: Text(label),
),
),
],
),
),
];
},
body: TabBarView(
controller: _tabCont,
children: [
..._tabs.map(
(label) => SamplePage(
label: label,
),
),
],
),
),
);
}
}
class SamplePage extends StatelessWidget {
const SamplePage({Key? key, required this.label}) : super(key: key);
final String label;
#override
Widget build(BuildContext context) {
return Container(
child: Center(child: Text('Page of $label')),
);
}
}
I'm learning Flutter and I am currently trying to make a home page with a cool scrolling effect. I'm trying to implement a CustomScrollView with 3 elements: a SliverAppBar, a horizontal scrolling list and a SliverList. The first two were easy enough and after some struggling I managed to implement the horizontal scrolling list by using a SliverPersistentHeader.
However, I ran into an issue. I want the SliverAppBar to be pinned and the SliverPersistentHeader containing the horizontal scrolling list to be floating. Everything works fine, except the floating element gets covered by the pinned one when scrolling back up after scrolling down. I basically want the floating element to "know" there is another element above it and offset itself when scrolling up.
You can see the issue here, alongside my code:
https://dartpad.dev/32d3f2a890d4a676decb014744fcc9ba
Make sure you click and drag to scroll in order to see the issue!
How can I fix this? Is there anything I am missing that causes this issue?
Thank you for your time!
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
// I had to change this class to a StatefulWidget to be able to listen to the scroll event
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
// Here I declared the ScrollController for the CustomScrollView
ScrollController _controller;
// And here is a boolean to check when the user scrolls up or down the view
bool sliverPersistentHeader = false;
#override
void initState() {
super.initState();
// The ScrollController is initialized in the initState and listens for when the user starts scrolling up and changes the boolean value accordingly
_controller = ScrollController();
_controller.addListener(() {
if (_controller.position.userScrollDirection == ScrollDirection.reverse) {
setState(() {
sliverPersistentHeader = false;
});
} else {
setState(() {
sliverPersistentHeader = true;
});
}
});
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _controller,
slivers: <Widget>[
SliverAppBar(
floating: true,
pinned: true,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text('App Title'),
),
),
SliverPersistentHeader(
// The SliverPersisitentHeader checks the boolean value and either pins or unpins the the Header
pinned: sliverPersistentHeader ? true : false,
delegate: CustomSliver(
expandedHeight: 150.0,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, index) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: Container(
height: 50.0,
color: Colors.amber,
),
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab1'),
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab2'),
),
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Tab3'))
],
currentIndex: 0,
),
);
}
}
class CustomSliver extends SliverPersistentHeaderDelegate {
final double expandedHeight;
CustomSliver({#required this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Scrollbar(
child: Container(
color: Theme.of(context).canvasColor,
padding: EdgeInsets.fromLTRB(10.0, 15.0, 0, 5.0),
child: ListView.separated(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.only(right: 10.0, top: 10.0, bottom: 10.0),
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(20.0)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.16),
offset: Offset(0, 3.0),
blurRadius: 6.0),
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.navigation),
Text(
'Category',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
],
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(width: 5.0);
},
)),
);
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => 150.0;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
The only thing I didn't do was to animate the SliverPersistentHeader into view, hopefully, you can achieve this yourself. I'm sure there are other ways to achieve this, but this solution should work for you.
For anyone that is still looking for a solution. You can try to implement this using a NestedScrollView and SliverOverlapAbsorber.
The following code will demonstrate this
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, isScrolled) => [
SliverAppBar(
pinned: true,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text('App Title'),
),
),
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverPersistentHeader(
floating: true,
delegate: CustomSliver(
expandedHeight: 150.0,
),
),
),
],
body: ListView.builder(
itemCount: 100,
itemBuilder: (_, index) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: Container(
height: 50.0,
color: Colors.amber,
),
),
),
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab1'),
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab2'),
),
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Tab3'))
],
currentIndex: 0,
),
);
}
}
class CustomSliver extends SliverPersistentHeaderDelegate {
final double expandedHeight;
CustomSliver({required this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Scrollbar(
child: Container(
color: Theme.of(context).canvasColor,
padding: EdgeInsets.fromLTRB(10.0, 15.0, 0, 5.0),
child: ListView.separated(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.only(right: 10.0, top: 10.0, bottom: 10.0),
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(20.0)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.16),
offset: Offset(0, 3.0),
blurRadius: 6.0),
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.navigation),
Text(
'Category',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
],
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(width: 5.0);
},
)),
);
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => 150.0;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
Notice the floatHeaderSlivers: true. On the NestedScrollView widget.
Notice the SliverOverlapAbsorber
Some resources to help
https://github.com/flutter/flutter/issues/62194#issuecomment-664625589
https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html
https://api.flutter.dev/flutter/widgets/SliverOverlapAbsorber-class.html