I want to use create a sliver list view with a SliverAppBar such that when I scroll up the list, the icon inside the body shrinks to take place in the leading space of appBar.
The images here show something that I want to achieve. When I scroll up, the chart should move up and slide beside the title. (Something similar to Hero widget)
Till now, I tried SliverAppBar, but was not able to succeed. I am happy to use some other widget to achieve this. Thank you.
Have you tried with this?
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
//Variables needed to adapt FlexibleSpaceBar text color (title)
ScrollController _scrollController;
bool lastStatus = true;
double height = 200;
void _scrollListener() {
if (_isShrink != lastStatus) {
setState(() {
lastStatus = _isShrink;
});
}
}
bool get _isShrink {
return _scrollController.hasClients &&
_scrollController.offset > (height - kToolbarHeight);
}
#override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_scrollListener);
}
#override
void dispose() {
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: height,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: _isShrink
? Row(
children: [
//Replace container with your chart
// Here you can also use SizedBox in order to define a chart size
Container(
margin: EdgeInsets.all(10.0),
width: 30,
height: 30,
color: Colors.yellow),
Text('A little long title'),
],
)
: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'A little long title',
textAlign: TextAlign.center,
),
//Replace container with your chart
Container(
height: 80,
color: Colors.yellow,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Your chart here'),
],
),
),
]),
),
),
),
];
},
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Container(
height: 40,
child: Text(index.toString()),
);
},
),
),
);
}
}
I don't think it can be done with SliverAppBar. You should search in Pub.dev for packages may this help you fluent_appbar
Related
I have a requirement to develop a screen where there is collapsible content to be achieved using sliver.
However, the height of collapsible content is dynamic and depends on the number of dynamic widgets applicable to the user (some may not have both dynamic widgets, some have one, some have both). These dynamic widgets load as parallel service to backend and not in sequential manner. Otherwise I would have calculated the height one by one.
Help would be appreciated since all examples on internet point to have a fixed header height for slivers
Example image attached of what i am trying to achieve.
Try with the silver appbar and make sure that your toolbarHeight is 0. Here I used just fixed height for a single element and the total height will be changed based on the number of elements or widgets you have.
import 'package:flutter/material.dart';
class DynamicAppbar extends StatefulWidget {
const DynamicAppbar({Key key}) : super(key: key);
#override
_DynamicAppbarState createState() => _DynamicAppbarState();
}
class _DynamicAppbarState extends State<DynamicAppbar> {
//set the height fixed for each widget
double fixedHeight = 50;
// replace with coming elements
List<String> items = [
"dynamicWidget1",
"dynamicWidget2",
"dynamicWidget3",
"dynamicWidget4",
];
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text("My App Bar"),
),
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: fixedHeight * items.length,
floating: false,
pinned: true,
snap: false,
toolbarHeight: 0,
flexibleSpace: FlexibleSpaceBar(
background: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: List<Widget>.generate(
items.length,
(index) {
return Container(
height: fixedHeight,
child: Center(
child: Text(
items[index],
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
)),
);
},
),
),
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
labelColor: Colors.black87,
unselectedLabelColor: Colors.grey,
tabs: [
Tab(icon: Icon(Icons.info), text: "Tab 1"),
Tab(icon: Icon(Icons.lightbulb_outline), text: "Tab 2"),
],
),
),
pinned: true,
),
];
},
body: Center(
child: Text("Sample text"),
),
),
),
),
);
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar);
final TabBar _tabBar;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
child: _tabBar,
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
Output:
Before and after scrolling
// this field is used for getting height of widget dynamically.
// you can set initial value as per your requirement.
var _cardSize = Size(Get.width, 300);
// this key is set to the widget of which we want to get size dynamically
GlobalKey _key = GlobalKey();
#override
void initState() {
super.initState();
//IMPORTANT---- this will be called once the build() method gets
// executed. By then we will have the widget rendered.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_getSizeOfWidget();
});
}
//Get the size of the widget
_getSizeOfWidget() {
try {
RenderBox _cardBox = _key.currentContext.findRenderObject();
_cardSize = _cardBox.size;
if (!mounted) return;
setState(() {});
} catch (e) {}
}
//Set the key to the widget for which you need to set height dynamically
//IMPORTANT -- set the height value using _cardSize
Container(key: _key,child : //any child,height : _cardSize.height);
I hope this answers your question
I have a TabBarView in the middle of the page which changes tab. The content on tab 1 is created by StaggeredGridView.countBuilder while content on 2nd tab is created by listview.builder.
The content of each tab is scrollable, however, only the content below the TabBarView is scrollable.
Is it possible that as I scroll the page, the TabBarView also moves from the middle of the screen to the top of the page and locks itself there, so that the entire screen can be filled with content from each tab?
I've seen people suggesting using SingleChildScrollView for the body and physics: NeverScrollableScrollPhysics() for the listview.builder.
This has not worked. The screen returns just the background color when it is run.
You need to use a custom scrollView which holds all the items.
Your profile details widget
tabBar
tabview
lets implement this
con: appbar cannot be pinned
import 'package:flutter/material.dart';
class MyCustomScrollViewScreen extends StatefulWidget {
#override
_MyCustomScrollViewScreenState createState() =>
_MyCustomScrollViewScreenState();
}
class _MyCustomScrollViewScreenState extends State<MyCustomScrollViewScreen>
with TickerProviderStateMixin {
TabController tabController;
#override
Widget build(BuildContext context) {
tabController = TabController(length: 2, vsync: this);
return Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
title: Text("AppBar"),
),
//profile widget
SliverToBoxAdapter(
key: UniqueKey(),
child: Container(
color: Colors.green,
height: 100,
child: Center(child: Text("Profile details")),
),
),
//tabbar
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: MyCustomHeader(
expandedHeight: kToolbarHeight,
tabs: TabBar(
controller: tabController,
tabs: [
Icon(
Icons.ac_unit,
size: 30,
color: Colors.black,
),
Icon(
Icons.access_alarm,
size: 30,
color: Colors.black,
),
],
))),
//children
SliverFillRemaining(
child: TabBarView(
controller: tabController,
children: [
Center(child: Text("I'm 1")),
Center(child: Text("I'm 2"))
],
),
)
],
),
),
);
}
}
/// persistent header
class MyCustomHeader extends SliverPersistentHeaderDelegate {
MyCustomHeader({
#required this.expandedHeight,
this.tabs,
this.context,
});
final Widget tabs;
final double expandedHeight;
final BuildContext context;
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
child: tabs,
);
}
}
here is the result
img url
I want to create a website using flutter web but I'm unable to navigate to sections in the same page. Here's an example of what I want to achieve using flutter.
P.S. Navigator is not working:
I created an example with PageView
class MyHomePage extends StatelessWidget {
var list = ["Home","Services", "Work", "About"];
var colors = [Colors.orange, Colors.blue, Colors.red, Colors.green];
PageController controller = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: 50,
height: 50,
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10)
),
),
Spacer(),
Row(
children: List.generate(3, (index){
return GestureDetector(
onTap: (){
_scrollToIndex(index);
},
child: Container(
margin: EdgeInsets.all(8),
child: Text(
list[index+1]
),
),
);
}),
)
],
),
Expanded(
child : PageView(
scrollDirection: Axis.vertical,
pageSnapping: false,
controller: controller,
children: List.generate(list.length, (index){
return Container(
width: MediaQuery.of(context).size.width,
height: double.maxFinite,
color: colors[index],
child: Center(
child: Text(
list[index],
style: TextStyle(
color: Colors.white,
fontSize: 50
),
),
),
);
})
),
),
],
)
),
);
}
void _scrollToIndex(int index) {
controller.animateToPage(index + 1, duration: Duration(seconds: 2), curve: Curves.fastLinearToSlowEaseIn);
}
}
The output:
ScrollController is the thing you are looking for.
Add a new one to your ScrolView and you can set where you want it to scroll to.
Josteve mentioned a way of doing it. But I'd like to show the other way which provides more features as one would expect in the gif example you have put.
You can see the demo here: https://mohith7548.github.io/portfolio/
My project has 3 sections called About, Blog & Projects. It also has another top section called Home. So the order of screens is Home, About, Blog & Projects. Each section takes full-screen height & width. So the starting offset for these pages are [0 * screenHeight, 1 * screenHeight, 2 * screenHeight, 3 * screenHeight] respectively. screenHeight can be accessed by MediaQuery.of(context).size.height inside build method.
class Portfolio extends StatefulWidget {
#override
_PortfolioState createState() => _PortfolioState();
}
class _PortfolioState extends State<Portfolio> {
ScrollController _scrollController;
String _curNavItem;
static double offsetHome = 0;
static double offsetAbout = SizeConfig.screenHeight;
static double offsetBlog = 2 * SizeConfig.screenHeight;
static double offsetProjects = 3 * SizeConfig.screenHeight;
#override
void initState() {
super.initState();
_scrollController = ScrollController();
}
#override
void dispose() {
super.dispose();
_scrollController.dispose();
}
void scrollTo(String title) {
double offset = 0;
switch (title) {
case Constants.HOME:
offset = offsetHome;
break;
case Constants.ABOUT:
offset = offsetAbout;
break;
case Constants.BLOG:
offset = offsetBlog;
break;
case Constants.PROJECTS:
offset = offsetProjects;
break;
}
setState(() {
_curNavItem = title;
});
// animate to the pag
_scrollController.animateTo(
offset,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOutQuart,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: PageScrollPhysics(), // use NeverScrollableScrollPhysics() to block user scrolling
controller: _scrollController,
slivers: <Widget>[
// This is just SliverAppBar wrapped in InterheritedWidget called NavState
// You can use just SliverAppBar
NavState(
curNavItem: _curNavItem,
scrollTo: scrollTo,
child: AppBanner(key: _appBannerKey), // SliverAppBar in another file
),
SliverList(
delegate: SliverChildListDelegate([
About(),
Blog(),
Projects(),
]),
)
],
),
);
}
}
You can do this in different ways:
TabBarView https://stackoverflow.com/a/60624536/10976088
PageView https://stackoverflow.com/a/60778791/10976088
NavigationRail https://api.flutter.dev/flutter/material/NavigationRail-class.html
My method: Using a state management way to keep name or index of content pages and change visible page. I do it with the Riverpod package here:
Suppose you want to have a fixed SidebarView and HeaderView in all pages and also you have a ContentPage that will be changed.
So you can have a RootPage including these 3 sections and change ContentPage by the riverpod, so that only ContentPage will be changed.
class RootPage extends StatelessWidget {
const RootPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: SidebarView(),
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Responsive.isDesktop(context))
const Expanded(
flex: 1,
child: SidebarView(),
),
Expanded(
flex: 5,
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
HeaderView(),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Consumer(
builder: (context, ref, _) {
var watch = ref.watch(pageVisibleStateProvider);
return contentPageSelection(watch.state);
},
),
),
),
],
),
),
),
],
),
);
}
}
simply change content page:
Widget contentPageSelection(String pageName){
switch(pageName){
case "page1":
return Page1();
case "page2":
return Page2();
case "page3":
return Page3();
default:
return DefaultPage();
}
}
where:
final pageVisibleStateProvider = StateProvider<String>((_) => "defaultPage");
and:
class SidebarView extends StatelessWidget {
const SidebarView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text("sidebar content"),
);
}
}
class HeaderView extends StatelessWidget {
const HeaderView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text("HeaderView content"),
);
}
}
Now you can change content page. for example you want to show Page2:
ElevatedButton(
onPressed: (){
ref.read(pageVisibleStateProvider.notifier).state = "page2";
},
child: Text("go to page 2"),
)
where page2 and other content pages only includes content not sidebar or header:
class Page2 extends StatelessWidget {
const Page2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text("page2 content");
}
}
I want to navigate from "Now Showing" to "Coming Soon" with a left swipe on the image, Moreover, I want the Appbar to not to move when I swipe, but I think it is only possible with tab bars and I am not sure, please give some advice if you know how to achieve this
enter image description here
As per GaboBrandX, he is correct. But you can also do one thing with the tabs also. The sliding will not work. It is complex, but you can give it a shot.
The picture I will give you, so there would be Tabs and below that there would be a container each container gets replaces by a click.
TabController controller;
int activeIndex = 0;
#override
void initState() {
super.initState();
this.tabController = TabController(length: 3, vsync: this);
}
//This changes the activeIndex based upon the tabController index
onTabChanged(){
this.setState((){
this.activeTabIndex = this.tabController.index;
});
}
//This will return your container, based upon your tabs selected
Widget getActiveTabView(){
case 1: {return YourSecondContainer();}
break;
default: {return YourFirstContainer();}
}
//Here is your full layout
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TMTabBar(titles: 'XYZ', controller: this.tabController, onChange: this.onTabChanged),
this.getActiveTabView(),
]
);
}
//Create a TabBarWidget and do this
class TMTabBar extends StatefulWidget {
String/List<String> titles;
TabController controller;
VoidCallback onChange;
TMTabBar({#required this.titles, #required this.controller, this.onChange});
#override
_TMTabBarState createState() => _TMTabBarState();
}
class _TMTabBarState extends State<TMTabBar> {
#override
void initState() {
//this is for changing the content as per the tabbar
this.widget.controller.addListener((){
if(this.widget.controller.indexIsChanging){
if(this.widget.onChange != null) this.widget.onChange();
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return TabBar(tab: YourTabs);
}
This basically gives you, what you're hoping for. Hope that helps. Thanks :)
Here I've made an example of what your looking for using a PageView. I've put only text on PageView's children, but you can put there your ListViews or anything you need. When tapping on a button the PageView navigates to the corresponding "page". This can be a starting point for you:
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
PageController _pageController = PageController(
initialPage: 0,
);
goToPage(num page) {
_pageController.animateToPage(
page,
duration: Duration(milliseconds: 350),
curve: Curves.easeIn,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: <Widget>[
Align(
alignment: Alignment.topCenter,
child: Container(
width: double.infinity,
height: 60.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: RaisedButton(
onPressed: () => goToPage(0),
child: Text('Now Showing'),
),
),
SizedBox(
width: 4.0,
),
Expanded(
child: RaisedButton(
onPressed: () => goToPage(1),
child: Text('Coming Soon'),
),
),
],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
height: MediaQuery.of(context).size.height - 60.0,
child: PageView(
controller: _pageController,
children: <Widget>[
Center(
child: Text('Tab 1'),
),
Center(
child: Text('Tab 2'),
),
],
),
),
),
],
),
),
);
}
}
I am trying to create a scrollView using CustomScrollView.
The effect that I need, is very similar to this one.
I need the SliverList to be stacked above the SliverAppbar, without the list taking the whole screen and hiding the SliverAppbar.
The reason I want to do this, is that i need to attach a persistent Positioned widget on top of that list, and it won't appear unless the list is stacked above the SliverAppbar.
Here's my code.
Step one:
Use ListView inside SliverAppBar widget. To make css overflow:hidden effect.
Step two:
Add controller to NestedScrollView and move the button on scrolling in a stack. Plus calculate where you want to stop button moving.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController scrollController;
final double expandedHight = 150.0;
#override
void initState() {
super.initState();
scrollController = new ScrollController();
scrollController.addListener(() => setState(() {}));
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
double get top {
double res = expandedHight;
if (scrollController.hasClients) {
double offset = scrollController.offset;
if (offset < (res - kToolbarHeight)) {
res -= offset;
} else {
res = kToolbarHeight;
}
}
return res;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
NestedScrollView(
controller: scrollController,
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
pinned: true,
expandedHeight: expandedHight,
flexibleSpace: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [
AppBar(
title: Text('AfroJack'),
elevation: 0.0,
),
Container(
color: Colors.blue,
height: 100,
alignment: Alignment.center,
child: RaisedButton(
child: Text('folow'),
onPressed: () => print('folow pressed'),
),
),
],
),
),
];
},
body: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 80,
itemBuilder: (BuildContext context, int index) {
return Text(
'text_string'.toUpperCase(),
style: TextStyle(
color: Colors.white,
),
);
},
),
),
Positioned(
top: top,
width: MediaQuery.of(context).size.width,
child: Align(
child: RaisedButton(
onPressed: () => print('shuffle pressed'),
child: Text('Suffle'),
),
),
),
],
),
);
}
}