Resize ListView during scrolling in Flutter - flutter

I'm trying to build a screen where two vertically stacked ListViews cause themselves to grow and shrink as a result of being scrolled. Here is an illustration:
The initial state is that both lists take up 50% of the top and bottom of the screen respectively. When the user starts dragging the top list downward (to scroll up) it will initially cause the list to expand to take up 75% of the screen before the normal scrolling behavior starts; when the user changes direction, dragging upwards (to scroll down), then as they get to the bottom of the list it will cause the list to shrink back up to only taking up 50% of the screen (the initial state).
The bottom list would work similarly, dragging up would cause the list to expand upwards to take up 75% of the screen before the normal scrolling behavior starts; when the user changes direction, dragging downwards (to scroll up), then as they get to the top of the list it will shrink back to 50% of the screen.
Here is an animation of what it should look like:
https://share.cleanshot.com/mnZhJF8x
My question is, what is the best widget combination to implement this and how do I tie the scrolling events with resizing the ListViews?
So far, this is as far as I've gotten:
Column(
children: [
SizedBox(
height: availableHeight / 2,
child: ListView(...)
),
Expanded(child: ListView(...)),
],
),
In terms of similar behavior, it appears that the CustomScrollView and SliverAppBar have some of the elements in scrolling behaving I'm going after but it's not obvious to me how to convert that into the the two adjacent lists view I described above.
Any advice would be greatly appreciated, thank you!

hi Check this,
Column(
children: [
Expanded (
flex:7,
child: Container(
child: ListView.builder(
itemCount:50,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("List item $index"));
}),
),
),
Expanded (
flex:3,
child: Container(
child: ListView.builder(
itemCount:50,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("aaaaaaaaa $index"));
}),
),
),
],
),

edit: refactored and maybe better version:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeight = 150.0,
});
final double height;
final double minHeight;
#override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
void incrementNotifier(ValueNotifier notifier, double increment) {
if (notifier.value + increment >= widget.height - widget.minHeight) return;
if (notifier.value + increment < widget.minHeight) return;
notifier.value += increment;
}
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null) {
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification) {
final delta = notification.scrollDelta ?? 0.0;
if (dragOnUpper) {
if (notification.metrics.extentAfter != 0) {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
} else {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
}
}
if (!dragOnUpper) {
if (notification.metrics.extentBefore != 0) {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
} else {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
}
}
}
return true;
}
#override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
here is the older post:
so, here's my shot on this. There might be a less complicated solution of course but I think it's somewhat understandable. At least I've tried to comment good enough.
Let me know if it works for you.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeightUpper = 300.0,
this.minHeightLower = 300.0,
});
final double height;
final double minHeightUpper;
final double minHeightLower;
#override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null)
// only act on ScrollStartNotification events with dragDetails
{
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification &&
notification.dragDetails != null)
// only act on ScrollUpdateNotification events with dragDetails
{
if (dragOnUpper) {
// dragging is going on, was started on upper ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (lowerSizeNotifier.value >= widget.minHeightLower)
// expand upper until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (notification.metrics.extentAfter == 0.0 &&
upperSizeNotifier.value > widget.minHeightUpper)
// when at the end of upper shrink it until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
if (!dragOnUpper) {
// dragging is going on, was started on lower ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (notification.metrics.extentBefore == 0.0 &&
lowerSizeNotifier.value > widget.minHeightLower)
// when at the top of lower shrink it until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (upperSizeNotifier.value >= widget.minHeightUpper)
// expand lower until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
}
return true;
}
#override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
I think it's working okayish so far but supporting the "fling" effect - I mean the acceleration when users shoot the scrollable until simulated physics slows it down again - would be really nice, too.

First, initialise two scroll controllers for two of your listviews. Then register a post-frame callback by using WidgetsBinding.instance.addPostFrameCallback to make sure that the scroll controller has been linked to a scroll view. Next, setup scroll listeners in that callback.
To listen to scrolling update you can use scrollController.addListener. Then use if-else cases to catch the position of the scroll, if scroll position equals to maxScrollExtent then the user scrolled bottom and its the other way round for minScrollExtent. Check my edited implementation below:
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ScrollController _scrollCtrl1 = ScrollController();
final ScrollController _scrollCtrl2 = ScrollController();
double height1 = 300;
double height2 = 300;
bool isLoading = true;
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
isLoading = false;
height1 = SizeConfig.blockSizeVertical! * 50;
height2 = SizeConfig.blockSizeVertical! * 50;
});
_scrollCtrl1.addListener(() {
if (_scrollCtrl1.position.pixels == _scrollCtrl1.position.maxScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 25;
height2 = SizeConfig.blockSizeVertical! * 75;
});
}
if (_scrollCtrl1.position.pixels == _scrollCtrl1.position.minScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 75;
height2 = SizeConfig.blockSizeVertical! * 25;
});
}
});
_scrollCtrl2.addListener(() {
if (_scrollCtrl2.position.pixels == _scrollCtrl2.position.maxScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 25;
height2 = SizeConfig.blockSizeVertical! * 75;
});
}
if (_scrollCtrl2.position.pixels == _scrollCtrl2.position.minScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 75;
height2 = SizeConfig.blockSizeVertical! * 25;
});
}
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
SizeConfig().init(context);
return Scaffold(
body: !isLoading ? Column(
children: [
AnimatedContainer(
color: Colors.blueGrey,
height: height1,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: ListView.builder(
itemCount: 50,
padding: EdgeInsets.zero,
controller: _scrollCtrl1,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
dense: true,
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("List item $index"));
}),
),
AnimatedContainer(
height: height2,
color: Colors.deepPurpleAccent,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: ListView.builder(
itemCount: 50,
padding: EdgeInsets.zero,
controller: _scrollCtrl2,
itemBuilder: (BuildContext context, int index) {
return ListTile(
dense: true,
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("aaaaaaaaa $index"));
}),
),
],
) : const Center(child: CircularProgressIndicator(),),
);
}
}
class SizeConfig {
static MediaQueryData? _mediaQueryData;
static double? screenWidth;
static double? screenHeight;
static double? blockSizeHorizontal;
static double? blockSizeVertical;
/// This class measures the screen height & width.
/// Remember: Always call the init method at the start of your application or in main
void init(BuildContext? context) {
_mediaQueryData = MediaQuery.of(context!);
screenWidth = _mediaQueryData?.size.width;
screenHeight = _mediaQueryData?.size.height;
blockSizeHorizontal = (screenWidth! / 100);
blockSizeVertical = (screenHeight! / 100);
}
}

Related

how to change the circular progress indicator after loading the data Flutter

the circular progress indicator dont disappear after loading the data .
this is my code where im using the progress indicator
and when i reach the end of the grid view it should load the other data but
the progress indicator makes the same thing it loads and dont disappear after getting data .
i tried to make a boolean isLoading and tried to change it true or false but couldnt find the place where i can do this
int pageNumber = 1;
String filterName = '';
class ShowsListDesign extends StatefulWidget {
#override
_ShowsListDesignState createState() => _ShowsListDesignState();
}
class _ShowsListDesignState extends State<ShowsListDesign> {
ScrollController controller = ScrollController();
ServicesClass service = ServicesClass();
bool isLoading = false;
#override
void initState() {
controller.addListener(listenScrolling);
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: service.getFilms('posts/$pageNumber/$filterName'),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
return Stack(
alignment: Alignment.bottomCenter,
children: [
GridView.builder(
itemCount: snapshot.data.length,
gridDelegate: const
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
crossAxisSpacing: 24,
mainAxisSpacing: 24,
childAspectRatio: (3 / 5),
),
controller: controller,
itemBuilder: (context, index) {
return FilmsCard(
image: snapshot.data[index]['thumbnailUrl'],
title: snapshot.data[index]['title'],
year: snapshot.data[index]['year'],
);
},
),
FloatingActionButton(
onPressed: () {
scrollUp();
},
elevation: 24,
backgroundColor: PRIMARY,
child: const Text(
'Scroll Up',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
),
),
),
],
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}
void scrollUp() {
const double start = 0;
controller.animateTo(start,
duration: const Duration(seconds: 1, milliseconds: 50),
curve: Curves.easeIn);
}
void listenScrolling() {
if (controller.position.atEdge) {
final isTop = controller.position.pixels == 0;
if (isTop) {
} else {
setState(() {
pageNumber++;
ShowsListDesign();
});
}
}
}
}
It is possible to get errors or no data on future, it will be better with handling those states.
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Stack(
alignment: Alignment.bottomCenter,
children: [...],
);
} else if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return const Text("couldn't find any data");
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
} else {
return const Text(" any other");
}
},
More about FutureBuilder class.
You can't use FutureBuilder if it's not a one page load. Try this:
(I don't understand your scrolling mechanism though), also call super.initState when you override
String filterName = '';
class ShowsListDesign extends StatefulWidget {
#override
_ShowsListDesignState createState() => _ShowsListDesignState();
}
class _ShowsListDesignState extends State<ShowsListDesign> {
ScrollController controller = ScrollController();
ServicesClass service = ServicesClass();
bool isLoading = false;
// New
int pageNumber = 1;
List? data;
Future<void> load() async {
setState(() {
isLoading = true;
});
data = await service.getFilms('posts/1/$filterName');
setState(() => isLoading = false);
}
#override
void initState() {
// Make sure you call super.initState() when you override
controller.addListener(listenScrolling);
super.initState();
load();
}
// Also call dispose to remove listener
#override
void dispose() {
controller.removeListener(listener);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Builder(
builder: (BuildContext context) {
if (data != null) {
return Stack(
alignment: Alignment.bottomCenter,
children: [
GridView.builder(
itemCount: data!.length,
gridDelegate: const
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
crossAxisSpacing: 24,
mainAxisSpacing: 24,
childAspectRatio: (3 / 5),
),
controller: controller,
itemBuilder: (context, index) {
return FilmsCard(
image: data![index]['thumbnailUrl'],
title: data![index]['title'],
year: data![index]['year'],
);
},
),
FloatingActionButton(
onPressed: () {
scrollUp();
},
elevation: 24,
backgroundColor: PRIMARY,
child: const Text(
'Scroll Up',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
),
),
),
],
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}
void scrollUp() {
const double start = 0;
controller.animateTo(start,
duration: const Duration(seconds: 1, milliseconds: 50),
curve: Curves.easeIn);
}
Future<void> listenScrolling() async {
// Change this depending on the scrolling works ?!
if (controller.position.pixels == controller.position.maxScrollExtent && data != null) {
List new_data = await service.getFilms('posts/${pageNumber + 1}/$filterName');
data.addAll(new_data);
setState(() {
pageNumber++;
});
}
}
}

flutter: How to modify the color of a Container in a large number of Containers

sorry for not being able to explain what I really want to mean in the title, but you will understand in the picture below and the code is what I have done.
import 'package:flutter/material.dart';
class ChangeColor extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _ChangeColorState();
}
}
class _ChangeColorState extends State<ChangeColor> {
List<Color> colorList = List(3);
int selectedId = 0;
void selectById(int id) {
setState(() {
selectedId = id;
});
}
void renderColor(int selectedId) {
for (int i = 0; i < 3; ++i) {
if (i == selectedId) {
colorList[i] = Colors.teal;
} else {
colorList[i] = Colors.red;
}
}
}
#override
Widget build(BuildContext context) {
Widget myContainer(int id, Color color) {
return InkWell(
child: Container(
width: 100, height: 100,
color: color,
),
onTap: () {
selectById(id);
},
);
}
renderColor(selectedId);
return MaterialApp(
home: Scaffold(
body: Center(
child: Row(
children: <Widget>[
myContainer(0, colorList[0]),
myContainer(1, colorList[1]),
myContainer(2, colorList[2]),
],
)
),
),
);
}
}
So my question is if I have a large number of Containers and even don't know the exact number, I can't give every Container an id and maybe can't use List, so how to solve the problem.
Actually this happens sometimes, for example in Calendar app. Thanks for any suggestion or criticism.
How about this way?
Widget myContainer(int id) {
return InkWell(
child: Container(
width: 100, height: 100,
color: selectedId == id ? Colors.teal : Colors.red,
),
onTap: () {
selectById(id);
},
);
}
It is a full code I fixed from your code.
And I recommend to move a 'myContainer' to outside of build().
class _ChangeColorState extends State<ChangeColor> {
List<Color> colorList = List(3);
int selectedId = 0;
void selectById(int id) {
setState(() {
selectedId = id;
});
}
Widget myContainer(int id) {
return InkWell(
child: Container(
width: 100,
color: selectedId == id ? Colors.teal : Colors.red,
),
onTap: () {
selectById(id);
},
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Container(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (context, index) {
return myContainer(index);
},
),
),
),
),
);
}
}

Removing and Add new Cards in the List -Flutter

I'm working in a Dating App. So, it's like a Tinder, basically.
We have the cards layout here. So, it's a List in a Stack with Cards Widgets.
In this example that I was following https://mightytechno.com/flutter-tinder-swipe-cards/ he create a List and when Swiped it is removed from List. But, when I try to add new cards, the cards aren't added.
My SwipeArea:
List<Widget> cards = List();
#override
void initState() {
super.initState();
cards.add(
new ProfileCard());
#override
Widget build(BuildContext context) {
return Expanded(
child: Container(
child: Stack(
children: cards,
),
));
}
void callBack(Widget value) {
setState(() {
cards.remove(value);
cards.add(new ProfileCard());
_adCardCounter++;
});
}
When the ProfileCard (card) is swiped, callBack is called, then I remove the Widget from List and Add a new one.
But, I can't see the Card in the Display.
If have 2 cards in a list, and I remove 2 and add 2, it display none cards.. but the array have 3.
Someone knows why? And how is the best solution to work with it? I tried to reuse the cards, but I can't.
You can copy past run full code below
You can use package https://pub.dev/packages/flutter_tindercard
You can add new card in swipeCompleteCallback
code snippet
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
_addToStream();
...
void _addToStream() {
Random random = new Random();
int index = random.nextInt(3);
print("index $index");
welcomeImages.add('https://picsum.photos/250?image=$index');
welcomeImages.removeAt(0);
_streamController.add(welcomeImages);
}
working demo
full code
import 'package:flutter/material.dart';
import 'package:flutter_tindercard/flutter_tindercard.dart';
import 'dart:async';
import 'dart:math';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AsyncDataExampleHomePage(),
);
}
}
// support for asynchronous data events
class AsyncDataExampleHomePage extends StatefulWidget {
#override
_AsyncDataExampleHomePageState createState() =>
_AsyncDataExampleHomePageState();
}
class _AsyncDataExampleHomePageState extends State<AsyncDataExampleHomePage>
with TickerProviderStateMixin {
StreamController<List<String>> _streamController;
List<String> welcomeImages = [
"https://picsum.photos/250?image=9",
"https://picsum.photos/250?image=10",
"https://picsum.photos/250?image=11",
];
#override
initState() {
super.initState();
_streamController = StreamController<List<String>>();
}
void _addToStream() {
Random random = new Random();
int index = random.nextInt(3);
print("index $index");
welcomeImages.add('https://picsum.photos/250?image=$index');
welcomeImages.removeAt(0);
_streamController.add(welcomeImages);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("asynchronous data events test"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Added image appears on top:',
),
StreamBuilder<List<String>>(
stream: _streamController.stream,
initialData: welcomeImages,
builder:
(BuildContext context, AsyncSnapshot<List<String>> snapshot) {
print('snapshot.data.length: ${snapshot.data.length}');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Add image');
case ConnectionState.waiting:
//return Text('Awaiting images...');
case ConnectionState.active:
print("build active");
return _AsyncDataExample(context, snapshot.data);
case ConnectionState.done:
return Text('\$${snapshot.data} (closed)');
}
return null; // unreachable
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _addToStream,
tooltip: 'Add image',
child: Icon(Icons.add),
),
);
}
Widget _AsyncDataExample(BuildContext context, List<String> imageList) {
CardController controller; //Use this to trigger swap.
print(imageList.length);
return Center(
key: UniqueKey(),
child: Container(
height: MediaQuery.of(context).size.height * 0.6,
child: TinderSwapCard(
orientation: AmassOrientation.TOP,
totalNum: imageList.length,
stackNum: 3,
swipeEdge: 4.0,
maxWidth: MediaQuery.of(context).size.width * 0.9,
maxHeight: MediaQuery.of(context).size.width * 0.9,
minWidth: MediaQuery.of(context).size.width * 0.8,
minHeight: MediaQuery.of(context).size.width * 0.8,
cardBuilder: (context, index) {
print("cardbuilder ${index}");
print("imageList length ${imageList.length}");
return Card(
color: Colors.blue,
child: Image.network('${imageList[index]}'),
);
},
cardController: controller = CardController(),
swipeUpdateCallback: (DragUpdateDetails details, Alignment align) {
/// Get swiping card's alignment
if (align.x < 0) {
//Card is LEFT swiping
} else if (align.x > 0) {
//Card is RIGHT swiping
}
},
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
_addToStream();
/// Get orientation & index of swiped card!
},
),
),
);
}
}

Preserve Widget State in PageView while enabling Navigation

I have a rather complex situation in a Flutter App.
I have a Home screen that is a swipable PageView,that displays 3 child Widgets : Calendar, Messages, Profile.
My issue at the moment is with the Calendar Widget. It is populated dynamically from the initState() method.
I managed to fix a first issue that came from swiping from one page to another that caused rebuilding the Calendar Widget every time.
My issue now is when I tap an item in the Calendar list, I open the detail view. Then, when I close it… all is still OK. However, when I swipe again the initState() method is called once more and the List view is rebuilt. I would like to prevent that and preserve it's state. any suggestions ?
Here is the Home code.
class HomeStack extends StatefulWidget {
final pages = <HomePages> [
CalendarScreen(),
MessagesScreen(),
ProfileScreen(),
];
#override
_HomeStackState createState() => _HomeStackState();
}
class _HomeStackState extends State<HomeStack> with AutomaticKeepAliveClientMixin<HomeStack> {
User user;
#override
bool get wantKeepAlive{
return true;
}
#override
void initState() {
print("Init home");
_getUser();
super.initState();
}
void _getUser() async {
User _user = await HomeBloc.getUserProfile();
setState(() {
user = _user;
});
}
final PageController _pageController = PageController();
int _selectedIndex = 0;
void _onPageChanged(int index) {
_selectedIndex = index;
}
void _navigationTapped(int index) {
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.ease
);
}
GestureDetector _navBarItem({int pageIndex, IconData iconName, String title}) {
return GestureDetector(
child: HomeAppBarTitleItem(
index: pageIndex,
icon: iconName,
title: title,
controller: _pageController
),
onTap: () => _navigationTapped(pageIndex),
);
}
Widget _buildWidget() {
if (user == null) {
return Center(
child: ProgressHud(imageSize: 70.0, progressSize: 70.0, strokeWidth: 5.0),
);
} else {
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_navBarItem(
pageIndex: 0,
iconName: Icons.calendar_today,
title: AppLocalizations.of(context).calendarViewTitle,
),
_navBarItem(
pageIndex: 1,
iconName: Icons.message,
title: AppLocalizations.of(context).messagesViewTitle,
),
_navBarItem(
pageIndex: 2,
iconName: Icons.face,
title: AppLocalizations.of(context).profileViewTitle,
),
],
),
backgroundColor: Colors.transparent,
elevation: 0.0,
),
backgroundColor: Colors.transparent,
body: PageView(
onPageChanged: (index) => _onPageChanged(index),
controller: _pageController,
children: widget.pages,
),
floatingActionButton: widget.pages.elementAt(_selectedIndex).fabButton,
);
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Stack(
children: <Widget>[
BackgroundGradient(),
_buildWidget(),
],
),
onWillPop: () async {
return true;
},
);
}
}
And the Calendar code.
class CalendarScreen extends StatelessWidget implements HomePages {
/// TODO: Prevent reloading
/// when :
/// 1) push detail view
/// 2) swipe pageView
/// 3) come back to calendar it reloads
static const String routeName = "/calendar";
static Color borderColor(EventPresence status) {
switch (status) {
case EventPresence.present:
return CompanyColors.grass;
case EventPresence.absent:
return CompanyColors.asher;
case EventPresence.pending:
return CompanyColors.asher;
default:
return CompanyColors.asher;
}
}
final FloatingActionButton fabButton = FloatingActionButton(
onPressed: () {}, /// TODO: Add action to action button
backgroundColor: CompanyColors.sky,
foregroundColor: CompanyColors.snow,
child: Icon(Icons.add),
);
#override
Widget build(BuildContext context) {
return CalendarProvider(
child: CalendarList(),
);
}
}
class CalendarList extends StatefulWidget {
#override
_CalendarListState createState() => _CalendarListState();
}
class _CalendarListState extends State<CalendarList> with AutomaticKeepAliveClientMixin<CalendarList> {
Events events;
void _getEvents() async {
Events _events = await CalendarBloc.getAllEvents();
setState(() {
events = _events;
});
}
#override
void initState() {
_getEvents();
super.initState();
}
#override
bool get wantKeepAlive{
return true;
}
Widget _displayBody() {
if (events == null) {
return ProgressHud(imageSize: 30.0, progressSize: 40.0, strokeWidth: 3.0);
} else if(events.future.length == 0 && events.past.length == 0) return _emptyStateView();
return EventsListView(events: events);
}
#override
Widget build(BuildContext context) {
return _displayBody();
}
Widget _emptyStateView() {
return Center(
child: Text("No data"),
);
}
}
class EventsListView extends StatefulWidget {
final Events events;
EventsListView({this.events});
#override
_EventsListViewState createState() => _EventsListViewState();
}
class _EventsListViewState extends State<EventsListView> {
GlobalKey _pastEventsScrollViewKey = GlobalKey();
GlobalKey _scrollViewKey = GlobalKey();
double _opacity = 0.0;
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderSliverList renderSliver = _pastEventsScrollViewKey.currentContext.findRenderObject();
setState(() {
CustomScrollView scrollView = _scrollViewKey.currentContext.widget;
scrollView.controller.jumpTo(renderSliver.geometry.scrollExtent);
_opacity = 1.0;
});
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: AnimatedOpacity(
opacity: _opacity,
duration: Duration(milliseconds: 300),
child: CustomScrollView(
key: _scrollViewKey,
controller: ScrollController(
//initialScrollOffset: initialScrollOffset,
keepScrollOffset: true,
),
slivers: <Widget>[
SliverList(
key: _pastEventsScrollViewKey,
delegate: SliverChildBuilderDelegate( (context, index) {
Event event = widget.events.past[index];
switch (event.type) {
case EventType.competition:
return CompetitionListItem(event: event);
case EventType.training:
return TrainingListItem(event: event);
case EventType.event:
return EventListItem(event: event);
}
},
childCount: widget.events.past.length,
),
),
SliverList(
delegate: SliverChildBuilderDelegate( (context, index) {
return Padding(
padding: EdgeInsets.only(top: 32.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Text(
DateFormat.MMMMEEEEd().format(DateTime.now()),
style: Theme.of(context).textTheme.body2.copyWith(
color: CompanyColors.snow,
),
),
);
},
childCount: 1,
),
),
SliverList(
delegate: SliverChildBuilderDelegate( (context, index) {
Event event = widget.events.future[index];
switch (event.type) {
case EventType.competition:
return CompetitionListItem(event: event);
case EventType.training:
return TrainingListItem(event: event);
case EventType.event:
return EventListItem(event: event);
}
},
childCount: widget.events.future.length,
),
),
],
),
),
);
}
}
From the documentation on AutomaticKeepAliveClientMixin:
/// A mixin with convenience methods for clients of
[AutomaticKeepAlive]. Used with [State] subclasses.
/// Subclasses must implement [wantKeepAlive], and their [build]
methods must call super.build (the return value will always return
null, and should be ignored).
So in your code, before you return the Scaffold just call super.build:
Widget build(BuildContext context) {
super.build(context);
return Scaffold(...);
}

How to create a listview that makes centering the desired element

I'm doing something similar to this video: https://youtu.be/fpqHUp4Sag0
With the following code I generate the listview but when using the controller in this way the element is located at the top of the listview and I need it to be centered
Widget _buildLyric() {
return ListView.builder(
itemBuilder: (BuildContext context, int index) => _buildPhrase(lyric[index]),
itemCount: lyric.length,
itemExtent: 90.0,
controller: _scrollController,
);
}
void goToNext() {
i += 1;
if (i == lyric.length - 1) {
setState(() {
finishedSync = true;
});
}
syncLyric.addPhrase(
lyric[i], playerController.value.position.inMilliseconds);
_scrollController.animateTo(i*90.0,
curve: Curves.ease, duration: new Duration(milliseconds: 300));
}
Using center and shrinkWrap: true
Center(
child: new ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return Text("Centered item");
},
),
);
You're going to have to do some math! (Nooooo, not the mathssssss).
It seems as though your goToNext() function is called while the app is running, rather than during build time. This makes it a little easier - you can simply use context.size. Otherwise you'd have to use a LayoutBuilder and maxHeight.
You can then divide this in two to get the half, then add/subtract whatever you need to get your item positioned how you want (since you've specified it's height as 90 in the example, I assume you could use 45 to get what you want).
Here's an example you can paste into a file to run:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(Wid());
class Wid extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Scrolling by time"),
),
body: new Column(
children: <Widget>[
Expanded(child: Container()),
Container(
height: 300.0,
color: Colors.orange,
child: ScrollsByTime(
itemExtent: 90.0,
),
),
Expanded(child: Container()),
],
),
),
);
}
}
class ScrollsByTime extends StatefulWidget {
final double itemExtent;
const ScrollsByTime({Key key, #required this.itemExtent}) : super(key: key);
#override
ScrollsByTimeState createState() {
return new ScrollsByTimeState();
}
}
class ScrollsByTimeState extends State<ScrollsByTime> {
final ScrollController _scrollController = new ScrollController();
#override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (timer) {
_scrollController.animateTo(
(widget.itemExtent * timer.tick) - context.size.height / 2.0 + widget.itemExtent / 2.0,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return Center(child: Text("Item $index"));
},
itemExtent: widget.itemExtent,
controller: _scrollController,
);
}
}
I had a similar problem, but with the horizontal listview. You should use ScrollController and NotificationListener. When you receive endScroll event you should calculate offset and use scroll controller animateTo method to center your items.
class SwipeCalendarState extends State<SwipeCalendar> {
List<DateTime> dates = List();
ScrollController _controller;
final itemWidth = 100.0;
#override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener);
for (var i = 1; i < 365; i++) {
var date = DateTime.now().add(Duration(days: i));
dates.add(date);
}
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
height: 200,
child: Stack(
children: <Widget>[buildListView()],
),
);
}
void _onStartScroll(ScrollMetrics metrics) {
}
void _onUpdateScroll(ScrollMetrics metrics){
}
void _onEndScroll(ScrollMetrics metrics){
print("scroll before = ${metrics.extentBefore}");
print("scroll after = ${metrics.extentAfter}");
print("scroll inside = ${metrics.extentInside}");
var halfOfTheWidth = itemWidth/2;
var offsetOfItem = metrics.extentBefore%itemWidth;
if (offsetOfItem < halfOfTheWidth) {
final offset = metrics.extentBefore - offsetOfItem;
print("offsetOfItem = ${offsetOfItem} offset = ${offset}");
Future.delayed(Duration(milliseconds: 50), (){
_controller.animateTo(offset, duration: Duration(milliseconds: 100), curve: Curves.linear);
});
} else if (offsetOfItem > halfOfTheWidth){
final offset = metrics.extentBefore + offsetOfItem;
print("offsetOfItem = ${offsetOfItem} offset = ${offset}");
Future.delayed(Duration(milliseconds: 50), (){
_controller.animateTo(offset, duration: Duration(milliseconds: 100), curve: Curves.linear);
});
}
}
Widget buildListView() {
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification) {
_onStartScroll(scrollNotification.metrics);
} else if (scrollNotification is ScrollUpdateNotification) {
_onUpdateScroll(scrollNotification.metrics);
} else if (scrollNotification is ScrollEndNotification) {
_onEndScroll(scrollNotification.metrics);
}
},
child: ListView.builder(
itemCount: dates.length,
controller: _controller,
scrollDirection: Axis.horizontal,
itemBuilder: (context, i) {
var item = dates[i];
return Container(
height: 100,
width: itemWidth,
child: Center(
child: Text("${item.day}.${item.month}.${item.year}"),
),
);
}),
);
}
}
IMO the link you have posted had some wheel like animation. Flutter provides this type of animation with ListWheelScrollView and rest can be done with the fade in animation and change in font weight with ScrollController.