In my app there is a DraggableScrollableSheet and a FAB. I want the FAB be invisible if the DraggableScrollableSheet is expanded. I need to check the event of expansion
I tried to attach a listener to the scrollController and check the value of scrollController.offset. But I realized that the listener is triggered just when the DraggableScrollableSheet is completly expanded and not before. Is there another way to check if it is expanded or collapsed?
double appbarSize = 0.08;
double offsetVisibility=100.0;
bool FAB_visibility=true;
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: Visibility(
visible: FAB_visibility,
child: FloatingActionButton(
child: Icon(Icons.add),
),
),
body: SizedBox.expand(
child: DraggableScrollableSheet(
maxChildSize: 0.8,
minChildSize: appbarSize,
initialChildSize: appbarSize,
builder: (BuildContext context, ScrollController scrollController) {
_scrollListener() {
if(FAB_visibility==false && scrollController.offset<=offsetVisibility){
setState(() {
FAB_visibility=true;
});
}
else if(FAB_visibility==true && scrollController.offset>offsetVisibility){
setState(() {
FAB_visibility=false;
});
}
}
scrollController.addListener(_scrollListener);
return Container(
decoration: new BoxDecoration(
color: Colors.red,
borderRadius: new BorderRadius.only(
topLeft: const Radius.circular(10.0),
topRight: const Radius.circular(10.0))),
child: ListView.builder(
controller: scrollController,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
);
},
)),
);
}
Thanks to flutter team I understood how to do it!
We should wrap DraggableScrollableSheet in a NotificationListener
This is the working code:
class _MyHomePageState extends State<MyHomePage> {
double appbarSize = 0.08;
double offsetVisibility = 100.0;
bool FAB_visibility = true;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox.expand(
child: NotificationListener<DraggableScrollableNotification>(
onNotification: (DraggableScrollableNotification DSNotification){
if(FAB_visibility && DSNotification.extent>=0.2){
setState(() {
FAB_visibility=false;
});
}
else if(!FAB_visibility && DSNotification.extent<0.2){
setState(() {
FAB_visibility=true;
});
}
},
child: DraggableScrollableSheet(
maxChildSize: 0.8,
minChildSize: appbarSize,
initialChildSize: appbarSize,
builder: (BuildContext context, ScrollController scrollController) {
return Container(
child: ListView.builder(
controller: scrollController,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
);
},
),
)),
floatingActionButton: Visibility(
visible: FAB_visibility,
child: FloatingActionButton(
child: Icon(Icons.add),
),
),
);
}
}
Related
I created a list view based on a Future Provider. It works as expected.
Now I want to add a ScrollController in order to create a animated FloatingActionButton like Gmail "Compose" button.
I put controller attribute on listView.builder.
And here I have weird behaviour : I can't scroll. As soon as I scroll down or up listview is rebuilding and I can't perform any scroll.
Here my code :
ScrollController _scrollController = ScrollController();
bool isFAB = false;
#override
void initState() {
_scrollController.addListener(() {
if (_scrollController.offset > 50) {
setState(() {
isFAB = true;
});
} else {
setState(() {
isFAB = false;
});
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.toolListTitle),
),
body: FutureBuilder(
future:
Provider.of<MyTools>(context, listen: false).fetchAndSetTools(),
builder: (ctx, snapshot) => snapshot.connectionState ==
ConnectionState.waiting
? const Center(
child: CircularProgressIndicator(),
)
: Consumer<MyTools>(
child: Center(
child: Text(AppLocalizations.of(context)!.noToolYet),
),
builder: (ctx, myTools, ch) => myTools.items.isEmpty
? Center(
child: Text(AppLocalizations.of(context)!.noToolYet),
)
: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.vertical,
itemCount: myTools.items.length,
itemBuilder: (ctx, i) => ToolWidget(
id: myTools.items[i].id,
name: myTools.items[i].name,
createdAt: myTools.items[i].createdAt,
description: myTools.items[i].description,
),
),
),
),
floatingActionButton: isFAB
? FloatingActionButton(
onPressed: () =>
Navigator.of(context).pushNamed(AddToolScreen.routeName),
child: Icon(
Icons.add_sharp,
color: Theme.of(context).primaryColor,
),
backgroundColor: Colors.black,
)
: FloatingActionButton.extended(
onPressed: () =>
Navigator.of(context).pushNamed(AddToolScreen.routeName),
icon: Icon(
Icons.add_sharp,
color: Theme.of(context).primaryColor,
),
backgroundColor: Colors.black,
label: Text(
"Add Tool",
style: TextStyle(
color: Theme.of(context).primaryColor,
),
),
));
}
}
Can you help me ?
Thanks
I think the
setState(() {
isFAB = true;
});
in your _scrollController.addListener function is resetting the scroll position to the top again.
I have the code below which feed a list with 10 results from firebase. In this case it shows only the 10 items, now I wanna, when user gets the bottom of results, it loads more 10 items and add it to the list. I already have the scrollController and it works.. I receive the log "LOAD HERE" when I get the bottom of the results.
My doubt is how to add the new 10 items in the list?
scrollListener() async {
if (scrollController.position.maxScrollExtent == scrollController.offset) {
print('LOAD HERE');
}
}
#override
void initState() {
scrollController.addListener(scrollListener);
super.initState();
}
#override
void dispose() {
scrollController.removeListener(scrollListener);
super.dispose();
}
loadList(submenu ,callback, context, deviceSize){
return FutureBuilder(
future: ctrlLab.loadList(submenu, 10),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.error != null) {
print(snapshot.error);
return Center(child: Text('ERROR!'));
}else {
return GridView.builder(
padding: EdgeInsets.all(10.0),
controller: scrollController,
itemCount: snapshot.data.length,
itemBuilder: (ctx, i) {
Item item = snapshot.data[i];
if (i < snapshot.data.length) {
return Dismissible(
key: UniqueKey(),
direction: DismissDirection.endToStart,
background: Container(
padding: EdgeInsets.all(10.0),
color: Colors.grey[800],
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Icon(
Icons.delete,
color: Colors.white,
size: 40,
),
),
),
onDismissed: (DismissDirection direction) {
ctrl.onDismissed(callback, item);
},
child: GestureDetector(
child: Card(
elevation: 5.0,
child: Padding(
padding: EdgeInsets.all(10.0),
child: GridTile(
child: Hero(
tag: "${item}",
child: item.imageUrl == null
? setIconLab(item)
: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: setIconLab(item),
placeholder: (ctx, url) =>
Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) =>
Image.asset('assets/images/noPhoto.jpg',
fit: BoxFit.cover),
),
),
footer: Container(
padding: EdgeInsets.all(8.0),
color: Colors.white70,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.name
),
),
],
),
),
),
),
),
),
);
}
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCountAndLoading(
itemCount: snapshot.data.length + 1,
crossAxisCount: deviceSize.width < 600 ? 2 : 3,
childAspectRatio: 0.7,
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
),
);
}
},
);
}
Infinite Scrolling in ListView
I have achieved this case by using the local field instead of getting data from firebase. Hope it will give you some idea.
import 'package:flutter/material.dart';
class ListViewDemo extends StatefulWidget {
ListViewDemo({Key key}) : super(key: key);
#override
_ListViewDemoState createState() => _ListViewDemoState();
}
class _ListViewDemoState extends State<ListViewDemo> {
ScrollController controller;
int count = 15;
#override
void initState() {
super.initState();
controller = ScrollController()..addListener(handleScrolling);
}
void handleScrolling() {
if (controller.offset >= controller.position.maxScrollExtent) {
setState(() {
count += 10;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List view'),
),
body: ListView.builder(
controller: controller,
itemCount: count,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
),
);
}
#override
void dispose() {
controller.removeListener(handleScrolling);
super.dispose();
}
}
You have to add another 10 data to the crtLap.loadList(subMenu, 20) and call setState inside the scrollListener to rebuild the widget about the changes.
var data = crtLap.loadList(subMenu, 10);
scrollListener() async {
if (scrollController.position.maxScrollExtent == scrollController.offset) {
setState((){
data = crtLap.loadList(subMenu, 20);
});
}
}
and use this data field to the FutureBuilder directly,
loadList(submenu ,callback, context, deviceSize){
return FutureBuilder(
future: data,
builder: (ctx, snapshot) {
.....
...
..
}
I have checkbox for selecting and deselecting photos.
There is a visible loading screen for each tap.
_mediaList has the photo asset. mediaModel has the necessary methods to add and remove the path of selected and deselected photos respectively.
Widget build(BuildContext context) {
super.build(context);
return GridView.builder(
itemCount: _mediaList.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, mainAxisSpacing: 4.0, crossAxisSpacing: 4.0),
itemBuilder: (BuildContext context, int index) {
final saved = mediaModel.getMedia().contains(
_mediaList[index].relativePath + '/' + _mediaList[index].title);
return FutureBuilder(
future: _mediaList[index].thumbDataWithSize(200, 200),
builder: (BuildContext context, snapshot) => snapshot.hasData
? GridTile(
header: saved
? Icon(Icons.check_circle, color: Colors.white,)
: Icon(Icons.check_circle_outline, color: Colors.white,),
child: GestureDetector(
child: Image.memory(
snapshot.data,
fit: BoxFit.cover,
),
onTap: () => setState(() => saved
? mediaModel.removeMedia(
_mediaList[index].relativePath +
'/' +
_mediaList[index].title)
: mediaModel.addMedia(
_mediaList[index].relativePath +
'/' +
_mediaList[index].title))),
)
: Container());
},
);
}
EDIT: After some analysis, I found out using Provider to load images might be the right way.
Can you help me in converting this code to Provider?
Thanks in advance!!!
Screenshot:
Full code:
class FooPage extends State<SoPage> {
static const int _count = 10;
final List<bool> _checks = List.generate(_count, (_) => false);
#override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.builder(
itemCount: _count,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (_, i) {
return Stack(
children: [
Container(color: Colors.red[(i * 100) % 900]),
Align(
alignment: Alignment.topCenter,
child: Checkbox(
value: _checks[i],
onChanged: (newValue) => setState(() => _checks[i] = newValue),
),
),
],
);
},
),
);
}
}
I want to know how to drag the DraggableScrollableSheet widget to go up or down programmatically. For example, based on a timer class.
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('DraggableScrollableSheet'),
),
body: SizedBox.expand(
child: DraggableScrollableSheet(
builder: (BuildContext context, ScrollController scrollController) {
return Container(
color: Colors.blue[100],
child: ListView.builder(
controller: scrollController,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
);
},
),
),
);
}
}
I have created bottom navigation in the flutter. When I switch bottom tabs, I don't see any animation there.
How can I add a custom animation to bottom navigation view? (Not the bottom navigation itself).
Bottom Navigation:
class Home extends StatelessWidget {
final String name;
const Home({Key key, #required this.name}) : super(key: key);
Widget _buildTabs(BuildContext context, activeTab) {
if (activeTab == AppTab.tasks) {
final TodosRepository todosRepository = TodosRepository();
return BlocProvider(
builder: (context) =>
TodosBloc(todosRepository: todosRepository)..dispatch(LoadTodos()),
child: Padding(
padding: const EdgeInsets.only(top: 64.0, left: 8.0, right: 8.0),
child: TasksTab(),
),
);
} else if (activeTab == AppTab.goals) {
return GoalsTab();
} else if (activeTab == AppTab.account) {
return Container(
child: Center(
child: Text('Account Tab'),
),
);
} else {
return Container();
}
}
#override
Widget build(BuildContext context) {
final tabBloc = BlocProvider.of<TabBloc>(context);
return BlocBuilder(
bloc: tabBloc,
builder: (BuildContext context, AppTab activeTab) {
return Scaffold(
body: _buildTabs(context, activeTab),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
tooltip: 'Add Todod',
),
bottomNavigationBar: TabSelector(
activeTab: activeTab,
onTabSelected: (tab) => tabBloc.dispatch(UpdateTab(tab)),
),
);
},
);
}
}
As you can see I have 3 tabs Task, Goals and Account.
Task tab:
class TasksTabState extends State<TasksTab> with TickerProviderStateMixin {
AnimationController _controller;
Animation _animation;
#override
void initState() {
super.initState();
print("Called when tab is changed");
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 100));
_animation = Tween(begin: -1.0, end: 0.0).animate(CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
))
..addStatusListener(handler);
}
#override
void dispose() {
_controller.dispose();
print("Component Disposed");
super.dispose();
}
void handler(status) {
print(status);
if (status == AnimationStatus.completed) {
_animation.removeStatusListener(handler);
_controller.reset();
_animation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
))
..addStatusListener(handler);
_controller.reverse();
}
}
Widget todoCard(Todo todo) {
print(todo.complete);
final double width = MediaQuery.of(context).size.width;
_controller.forward();
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Transform(
transform:
Matrix4.translationValues(_animation.value * width, 0.0, 0.0),
child: ExpansionTile(
leading: CircularCheckBox(
activeColor: Colors.redAccent,
value: todo.complete,
materialTapTargetSize: MaterialTapTargetSize.padded,
onChanged: (value) {
print(value);
print("Current todo: $todo");
}),
title: new Text(
todo.task,
style: new TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic),
),
children: <Widget>[
new Column(
children: [Text(todo.note)],
),
],
));
});
}
Widget _buildTodosList(context, snapshots) {
return ListView.builder(
itemCount: snapshots.length,
physics: ScrollPhysics(),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return todoCard(Todo.fromSnapshot(snapshots[index]));
},
);
}
#override
Widget build(BuildContext context) {
return BlocBuilder(
bloc: BlocProvider.of<TodosBloc>(context),
builder: (BuildContext context, TodosState state) {
if (state is TodosLoading) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
if (state is LoadTodos) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
if (state is TodosLoaded) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Project Title'),
Text('Project description'),
StreamBuilder(
stream: state.snapshotStream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return !snapshot.hasData
? Center(
child: CircularProgressIndicator(),
)
: _buildTodosList(context, snapshot.data.documents);
},
),
],
),
);
}
},
);
}
}
In task tabs, task cards are sliding right when I click the task tab.
But I click the Goals tab I want to achieve slide left animation of the task tab items.