Switch horizontal tabs on vertical scroll flutter - flutter

How can I switch tabs on a vertical scroll as shown above. What would be the best way to do this? And upon clicking on certain tab, control should be transfer to selected tab content. I tried to find solution on google but in vain.
Currently my code looks something like this but I know thats not it.
class SingleRestView extends StatefulWidget {
Restaurant singleRestaurant;
SingleRestView({#required this.singleRestaurant});
#override
_SingleRestViewState createState() => _SingleRestViewState();
}
class _SingleRestViewState extends State<SingleRestView>
with SingleTickerProviderStateMixin {
var _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
#override
Widget build(BuildContext context) {
var tabBar = TabBar(
controller: _tabController,
//This property will allow your tabs to be horizontally scrollable
isScrollable: true,
indicatorColor: Colors.black,
labelColor: Colors.black,
tabs: [
Text("Tab 1"),
Text("Tab 2"),
Text("Tab 2"),
],
);
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, isScrolled) => [
SliverAppBar(
expandedHeight: 300,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('Title'),
//Without this padding the title appears behind the tabs.
//This is the whole reason why the tabBar is in a local variable, to
//be able to use its height here.
titlePadding: EdgeInsetsDirectional.only(
start: 72, bottom: tabBar.preferredSize.height + 16),
// background:
),
// I did this this way to have a white bottom bar like in your photo,
// however, you could just pass the tabBar. The background image would
//be behind it.
bottom: PreferredSize(
child: Container(
//This will keep your tabs centered if you have not enough to fill
//the display's width
alignment: Alignment.center,
width: double.infinity,
color: Colors.white,
child: tabBar,
),
preferredSize: Size(double.infinity, tabBar.preferredSize.height),
),
),
],
body: TabBarView(
controller: _tabController,
children: <Widget>[
Container(height: 50,width: 100,color: Colors.red,),
Container(height: 50,width: 100,color: Colors.blue,),
Container(height: 50,width: 100,color: Colors.green,),
],
),
),
);
}
}
Video of what i want to achieve

nestedScrollController has a property called offset which will give you the current offset.
nestedScrollController.offset
But remember, screen sizes can be different for different devices. So using only offset will not help.
1. Naive Option using MediaQuery
With MediaQuery you can get to know the height of your device & then you can globally change the tab_index based on a comparison between offset & the height of the device. You can use some threshold value after which horizontal tab_index changes. Setting proper offset values might be a tough job !
double new_height = MediaQuery.of(context).size.height;
if(new_height/2<=offset)
{
//change my tab_index to 2
}
2. Using Listeners in Controller
Combining listeners & offsets
_TabController.addListener(() {
if (_controller.position.atEdge) {
if (_controller.position.pixels == 0) {
// You're at the top so tab_index=1
}
//At the bottom of screen
if(_controller.offset>=_controller.position.maxScrollExtent)
{
//set your tab_index
}
//middle of screen multiply by 0.5
if(_controller.offset>=_controller.position.maxScrollExtent*0.5)
{
//set your tab_index
}
//3/4 of screen multiply by 0.75
if(_controller.offset>=_controller.position.maxScrollExtent*0.75)
{
//set your tab_index
}
}
Further Reading -
https://medium.com/#diegoveloper/flutter-lets-know-the-scrollcontroller-and-scrollnotification-652b2685a4ac
Scroll Position Class
How to check if scroll position is at top or bottom in ListView?

Maybe you can try to use this plugin => https://pub.dev/packages/vertical_scrollable_tabview/score
import 'package:vertical_scrollable_tabview/vertical_scrollable_tabview.dart';
// Required it
TabBar(
onTap: (index) {
VerticalScrollableTabBarStatus.setIndex(index); <- Required
},
)
and
VerticalScrollableTabView(
tabController: tabController, <- Required TabBarController
listItemData: data, <- Required List<dynamic>
eachItemChild: (object,index){
return CategorySection(category: object as Category); <- Object and index
},
verticalScrollPosition: VerticalScrollPosition.begin,
),
I think this plugin can solve it.

Related

Display multiple pages at the same time

I have an app with two features, that have routes such as:
/feature1
/feature1/a
/feature2
/feature2/a
/feature2/a/b
/feature2/c
I can use GoRouter and its ShellRoute to switch between these one at a time using context.goNamed('feature2'), which would replace the entire screen with feature 2 (when tapping a tab in a tab bar for example). Here's a diagram of just the top level routes using tabs:
However, I would like to have an overview style menu which displays multiple destinations at once, so the user can see where they will be going before they go there (for example the preview page tabs in a mobile web browser). Here's a diagram:
and then tapping on either of the two pages would make them full screen:
Pressing the menu button at the bottom would return you to the overview menu page.
One way I have thought about solving this would be to make static preview images out of the routes when the menu button is tapped, and just display the previews. But these won't be live, and I would like a more elegant approach that actually displays the live contents of the route if possible.
Another way I have thought about solving this would be to use a top level GoRouter and then two descendant GoRouters each containing just one branch of the routes. I'm not sure if multiple GoRouters would lead to problems with things like if I wanted to context.go() to another branch.
If the ShellRoute.builder gave me access to all of the child page's widgets, I could display them however I wanted, but it just provides a single child.
I have not worked with 'go_router' or 'ShellRoute.builder', but I like to make custom animated widgets like this for apps. It's also hard to explain how it would work in your app, but here is my take on this.
Try copy pasting this in an empty page. I have written some notes in code comments that might help explain things a little bit. And, this is not perfect but with more polishing according to the needs it could work.
class CustomPageView extends StatefulWidget {
const CustomPageView({Key? key}) : super(key: key);
#override
State<CustomPageView> createState() => _CustomPageViewState();
}
class _CustomPageViewState extends State<CustomPageView> {
// Scroll Controller required to control scroll via code.
// When user taps on the navigation buttons, we will use this controller
// to scroll to the next/previous page.
final ScrollController _scrollController = ScrollController();
// Saving screen width and height to use it for the page size and page offset.
double _screenWidth = 0;
double _screenHeight = 0;
// A bool to toggle between full screen mode and normal mode.
bool _viewFull = false;
#override
void initState() {
super.initState();
// Get the screen width and height.
// This will be used to set the page size and page offset.
// As of now, this only works when page loads, not when orientation changes
// or page is resized. That requires a bit more work.
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_screenWidth = MediaQuery.of(context).size.width;
_screenHeight = MediaQuery.of(context).size.height;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
// 'Column' to wrap the 'Body' and 'BottomNavigationBar'
body: Column(
children: [
// 'Expanded' to take up the remaining space after the 'BottomNavigationBar'
Expanded(
// A 'Container' to wrap the overall 'Body' and aligned to center.
// So when it resizes, it will be centered.
child: Container(
alignment: Alignment.center,
// 'AnimatedContainer' to animate the overall height of the 'Body'
// when user taps on the 'Full Screen' button.
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: _viewFull ? 200 : _screenHeight,
// A 'ListView' to display the pages.
// 'ListView' is used here because we want to scroll horizontally.
// It also enables us to use 'PageView' like functionality, but
// requires a bit more work, to make the pages snap after scrolling.
child: ListView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
children: [
// A 'Container' to display the first page.
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
color: Colors.blue,
),
// A 'Container' to display the second page.
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
color: Colors.yellow,
),
],
),
),
),
),
// 'BottomNavigationBar' to show the navigation buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 'Feature 1' button
GestureDetector(
onTap: () {
// Scroll to the first page
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.red,
padding: const EdgeInsets.all(12),
child: const Text('Feature 1'),
),
),
// 'Feature 2' button
GestureDetector(
onTap: () {
// Scroll to the second page
_scrollController.animateTo(
_screenWidth,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.green,
padding: const EdgeInsets.all(12),
child: const Text('Feature 2'),
),
),
// 'Full Screen' button
GestureDetector(
onTap: () {
// Toggle between full screen mode and normal mode
setState(() {
_viewFull = !_viewFull;
});
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.purple,
padding: const EdgeInsets.all(12),
child: const Text('View Full'),
),
),
],
),
],
),
);
}
}

How to create custom header which hides and shows on scroll, like Facebook app

I'm trying to replicate a custom header, above a ListView, which hides and shows when the user scrolls the list. Very specifically, I want it to behave like the Facebook app... as soon as the user scrolls down, the header slides up out of view. Then, no matter how far down the list you are, when you scroll back up the header slides back into view immediately.
I've been playing with various Slivers, AnimatedContainers etc, but I can't get this exact behaviour.
SliverAppBar seems the closest, but it seems to have a predetermined structure, and I can't see a way to make it completely customizable.
SliverPersistentHeader and SliverToBoxAdapter both seem to remain fixed in place, and don't reappear when you scroll back up.
Any ideas on now to achieve this please?
I made something like this a while ago but only with a searchbar as the content of the SliverAppBar and because the SliverAppBar needs a predetermined size to be built I used a work-around like this.
class _ListScreenState extends State<ListScreen> {
final GlobalKey _flexibleSpaceBarKey = GlobalKey();
late Size sizeFlexibleSpaceBar;
bool _visible = true;
getSizeAndPosition() {
RenderBox _cardBox = _flexibleSpaceBarKey.currentContext!.findRenderObject() as RenderBox;
sizeFlexibleSpaceBar = _cardBox.size;
}
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) => getSizeAndPosition());
Future.delayed(Duration(microseconds: 1)).then((value) {
setState(() {
_visible = false;
});
});
}
Widget _copyFlexibleSpaceBar() {
return Visibility(
visible: _visible,
key: _flexibleSpaceBarKey,
child: _buildSearchbar(),
);
}
Widget _buildSearchbar() {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 12.0,
),
child: MyCustomSearchBar(),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('Example-Title'),
elevation: 0.0,
),
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
_copyFlexibleSpaceBar(),
if (!_visible)
Expanded(
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) =>
<Widget>[
SliverAppBar(
pinned: true,
floating: true,
toolbarHeight: 0.0,
expandedHeight: sizeFlexibleSpaceBar.height,
elevation: 4.0,
flexibleSpace: FlexibleSpaceBar(
background: _buildSearchbar(),
),
),
],
body: Container() // Your content
),
),
],
),
),
);
}
}
(Not sure if it works because I copied it from my git-repo and changed/removed some stuff)
So what I did there was, to build my custom header and immediately make it invisible, thus I can get the actual size and can use it to build the SliverAppBar.
I have to add, this idea is not mine, I got it from another post which I can't find right now.
Hope it somehow helps you.

How to add Grids into TabbarView in Flutter?

So basically I have this widget:
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// ...
SliverToBoxAdapter(
child: TabBar(
controller: this._controller,
indicator: UnderlineTabIndicator(
borderSide: BorderSide(width: 1.0, color: Colors.red),
),
tabs: [
Tab(icon: Icon(CupertinoIcons.camera)),
Tab(icon: Icon(CupertinoIcons.photo)),
Tab(icon: Icon(CupertinoIcons.video_camera)),
],
),
),
SliverFillRemaining(
child: TabBarView(
controller: this._controller,
children: [
// Want Scrollable Grid here
// Want Scrollable Grid here
Center(
child: Text("Hello Reader🙂"),
),
],
),
),
// ...
],
),
);
}
}
I want to add a 2 scrollable grids as children in the TabBarView however when I use GridView.builder(...), there is an annoying gap at the top of the grid and scrolling isn't all too great neither even with shrinkWrap: true and physics: NeverScrollableScrollPhysics().
However when I use a SliverGrid(...), there is this error
RenderObjects expect specific types of children because they coordinate with their children during layout and paint. For example, a RenderSliver cannot be the child of a RenderBox because a RenderSliver does not understand the RenderBox layout protocol.
This obviously makes sense because TabBarView isn't a sliver widget. I have already taken a look at this post but it wasn't really of any help.
How could I implement this? Is there perhaps a way I could create my own widget builder that builds a custom layout?
Thank You!
You need to use SliverOverlapAbsorber/SliverOverlapInjector, the following code works for me (working full code on dart pad):
Here i used SliverFixedExtentList but you can it replace with SliverGrid.
#override
State<StatefulWidget> createState() => _NewsScreenState();
}
class _NewsScreenState extends State<NewsScreen> {
final List<String> listItems = [];
final List<String> _tabs = <String>[
"Featured",
"Popular",
"Latest",
];
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
sliver: SliverAppBar(
title: const Text('Books'),
floating: true,
pinned: true,
snap: false,
primary: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
),
),
];
},
body: TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
// This Builder is needed to provide a BuildContext that is "inside"
// the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
// find the NestedScrollView.
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber above.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 60.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return Container(
color: Color((math.Random().nextDouble() *
0xFFFFFF)
.toInt() <<
0)
.withOpacity(1.0));
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
),
);
}
}

how to get a full size height of customscrollview widget in flutter?

I want to get the whole height of CustomScrollView widget. So I made a below code but it's not working.
#override
void initState(){
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => getSizeAndPosition());
}
getSizeAndPosition() {
RenderBox _customScrollBox =
_customScrollKey.currentContext.findRenderObject();
_customScrollSize = _customScrollBox.size;
_customScrollPosition = _customScrollBox.localToGlobal(Offset.zero);
print(_customScrollSize.height);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _customScrollKey,
appBar: _appbar(),
body: CustomScrollView(
controller: _controller,
slivers: [
SliverList(
delegate: SliverChildListDelegate([
_titleSection(),
_thumnail(),
SizedBox(
height: 40,
),
])),
],
),
);
}
The height obtained by this code does not take into account the height of the list in the customScrollview. I mean, _customScrollSize.height and MediaQuery.of(context).size.width are the same.
I want this function
_controller.addListener(() {
setState(() {
if (_controller.offset < 0) {
scrollHeight = 0;
} else {
scrollHeight = _controller.offset;
}
});
});
Container(
width: size.width * (scrollHeight / _customScrollSize.height),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1)),
With the above code, '_customScrollSize.height' does not reflect the overall height and therefore the function is not implemented properly. Is there any good way to use it in this situation?
CustomScrollView
A ScrollView that creates custom scroll effects using slivers.
A CustomScrollView lets you supply slivers directly to create various scrolling effects, such as lists, grids, and expanding headers. For example, to create a scroll view that contains an expanding app bar followed by a list and a grid, use a list of three slivers: SliverAppBar, SliverList, and SliverGrid.
In your case do remove the appBar and use a sliverAppBar withing the custome scrollview , and you can use sliverfillRemaining widget for your other children
example
CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Demo'),
),
),
SliverList(
delegate: SliverChildListDelegate([
_titleSection(),
_thumnail(),
SizedBox(
height: 40,
),
])),
...

Can flutter tabs have different widths and distribute the remaining space evenly as right and left paddings?

This is the way i tried to do it, but i obviously need the paddings to be calculated somehow. I'm thinking in a worst case scenario creating some sort of function that calculates all that, but can i select elements and get values?
This is roughly how i want them to look like (space between should vary in accordance to screen sizes) https://i.imgur.com/huhC9vn.png
I'm also thinking maybe some screens might not be able to fit it since the emulated one is pretty big, in which case i'm also thinking of having a minimum padding size and just make them scrollable.
bottom: TabBar(
labelPadding: EdgeInsets.only(left: 8, right: 8),
isScrollable: true,
tabs: <Widget>[
Tab(
text: 'General',
),
Tab(
text: 'Financial',
),
Tab(
text: 'Experts & Participants',
),
],
)),
The way the TabBar is implemented, doesn't allow to set flexible tabs. So you have to calculate the horizontal padding for each tab.
You'll need to get the tab widths, so you could do this:
Set a GlobalKey to each Tab and then access to
key.currentContext.size.width.
Since key.currentContext would be null during build, you need to
wait until the TabBar is rendered and then get the tabs width. To do
that, you could use WidgetsBinding.instance.addPostFrameCallback()
inside initState.
If the device orientation can change, you need to calculate the padding
again, so you could do that inside didChangeDependencies instead of
initState.
After you calculate the padding, you need to store that padding in a
variable and call setState to update the padding of the tabs.
To avoid calling setState in the entire tree, you could put the
TabBar in a custom StatefulWidget.
To be able to do this with any TabBar in the app, you could create a
custom TabBar widget, that receives the TabBar and return a new one
with the calculated padding.
So, depending on your case, you could implement some or all of the points mentioned. Here is an example with all the points mentioned:
class TabBarWithFlexibleTabs extends StatefulWidget implements PreferredSizeWidget {
TabBarWithFlexibleTabs({this.child});
final TabBar child;
#override
Size get preferredSize => child.preferredSize;
#override
_TabBarWithFlexibleTabsState createState() => _TabBarWithFlexibleTabsState();
}
class _TabBarWithFlexibleTabsState extends State<TabBarWithFlexibleTabs> {
final _tabs = <Widget>[];
final _tabsKeys = <Tab, GlobalKey>{};
var _tabsPadding = 0.0;
void _updateTabBarPadding() => setState(() {
final screenWidth = MediaQuery.of(context).size.width;
final tabBarWidth = _tabsKeys.values
.fold(0, (prev, tab) => prev + tab.currentContext.size.width);
_tabsPadding = tabBarWidth < screenWidth
? ((screenWidth - tabBarWidth) / widget.child.tabs.length) / 2
: widget.child.labelPadding?.horizontal ?? 16.0;
});
#override
void initState() {
super.initState();
widget.child.tabs.forEach((tab) => _tabsKeys[tab] = GlobalKey());
_tabs.addAll(widget.child.tabs
.map((tab) => Container(key: _tabsKeys[tab], child: tab)));
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) => _updateTabBarPadding());
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: widget.child.tabs.length,
child: TabBar(
tabs: _tabs,
isScrollable: true,
labelPadding: EdgeInsets.symmetric(
horizontal: _tabsPadding,
vertical: widget.child.labelPadding?.vertical ?? 0,
),
// TODO: pass other parameters used in the TabBar received, like this:
controller: widget.child.controller,
indicatorColor: widget.child.indicatorColor,
),
);
}
}
And you could use it like this:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBarWithFlexibleTabs(
child: TabBar(
tabs: <Widget>[
Tab(
text: 'General',
),
Tab(
text: 'Financial',
),
Tab(
text: 'Experts & Participants',
),
],
),
)
),
);
}
Notes:
The TabBar will be scrollable if it can't fit the screen width.
Check the // TODO: pass other parameters used in the TabBar received.
You may need to remove the DefaultTabController if you want to use
a custom one.