I want to create widget which images are displayed in a row.
Width of each images are same as device width.
So I created listview which has image widget.
but some of images are bigger than device height and image is cropped.
What can I do for my flutter project?
This is what I tried.
ListView.builder(
shrinkWrap: true,
scrollDirection:
widget.options.isHorizontal! ? Axis.horizontal : Axis.vertical,
physics:
widget.options.isHorizontal! ? const PageScrollPhysics() : null,
itemCount: widget.items?.length,
itemBuilder: (BuildContext context, int index) {
return Image.memory(
widget.items![index],
width: widget.options.isHorizontal!
? null
: MediaQuery.of(context).size.width,
height: widget.options.isHorizontal!
? MediaQuery.of(context).size.height
: null,
fit:
widget.options.isHorizontal! ? BoxFit.contain : BoxFit.fitWidth,
);
},
)
you can use ConstrainedBox
this is example code.
The JumpTo part of this example is not perfect yet, I hope you can realize it by learning ScrollNotification.
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late ScrollController _controller;
bool lock = false;
#override
void initState() {
_controller = ScrollController();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
var maxWidth = MediaQuery.of(context).size.width;
var maxHeight = MediaQuery.of(context).size.height;
return Scaffold(
body: NotificationListener<ScrollNotification>(
child: ListView.builder(
controller: _controller,
physics: ClampingScrollPhysics(),
itemCount: 20,
itemExtent: maxWidth,
itemBuilder: (context, index) {
return ConstrainedBox(
constraints:
BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight),
child: Image.network('https://picsum.photos/600'));
},
scrollDirection: Axis.horizontal,
),
onNotification: (ScrollNotification notification) {
if (notification is ScrollStartNotification) {
lock = false;
}
if (notification is ScrollEndNotification &&
!lock &&
_controller.position.isScrollingNotifier.value &&
_controller.position.hasPixels) {
lock = true;
var current = _controller.offset;
var diffIndex = (current / maxWidth).floor();
var isNext = ((current / maxWidth).toDouble() * 0.001) > 0.5;
var nexPosition = 0.0;
if (isNext) {
nexPosition = (diffIndex + 1) * maxWidth;
} else {
nexPosition = diffIndex * maxWidth;
}
_controller.removeListener(() {});
Future.delayed(Duration.zero, () {
// _controller.jumpTo(nexPosition);
_controller.animateTo(nexPosition,
duration: Duration(milliseconds: 400),
curve: Curves.fastOutSlowIn);
}).whenComplete(() => _controller.addListener(() {}));
}
return lock;
},
),
);
}
}
Related
I'm using a list view to display some items and also I'm using flutter_tindercard
to approve or refuse items when an item is approved it should be added to the approved items list then removed from the list view , and the same for refused items , but the issue when the item is refused or approved it is not removing from the list view.
PS: the bottom elements are not moving to the top
my code:
import 'package:flutter/material.dart';
import 'package:flutter_tindercard/flutter_tindercard.dart';
class ExampleHomePage extends StatefulWidget {
#override
_ExampleHomePageState createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage>
with TickerProviderStateMixin {
List approved = [];
List refused = [];
List<String> welcomeImages = [
"https://images.unsplash.com/photo-1631636176993-759dea1a1300?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw0fHx8ZW58MHx8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631634176568-f543af6a41de?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw3fHx8ZW58MHx8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631691971564-adf9419d904e?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwxM3x8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631641906574-24adb8594649?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwxMnx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1593642702821-c8da6771f0c6?ixid=MnwxMjA3fDF8MHxlZGl0b3JpYWwtZmVlZHwxMXx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631621461675-e61a1f28d3d6?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw0NHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60"
];
List<TinderCarder> cards = [];
CardController controller = CardController(); //Use this to trigger swap.
#override
void initState() {
// TODO: implement initState
super.initState();
cards = welcomeImages.map((element) {
return TinderCarder(
image: element,
controller: controller,
onLeftPress: () {
setState(() {
cards.removeAt(element.indexOf(element));
print('index is: ' + "${element.indexOf(element)}");
print('list length: ' + cards.length.toString());
});
},
onRightPress: () {
setState(() {
cards.removeAt(element.indexOf(element));
print('index is: ' + "${element.indexOf(element)}");
print('list length: ' + cards.length.toString());
});
},
);
}).toList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
child: ListView(
children: cards,
),
),
);
}
}
class TinderCarder extends StatelessWidget {
final String image;
Function onLeftPress;
Function onRightPress;
var controller;
TinderCarder(
{required this.image,
this.controller,
required this.onLeftPress,
required this.onRightPress});
#override
Widget build(BuildContext context) {
return Container(
height: 200,
child: TinderSwapCard(
orientation: AmassOrientation.BOTTOM,
totalNum: 1,
stackNum: 3,
swipeEdge: 4.0,
maxWidth: double.infinity,
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) => Card(
child: Image.network(
image,
width: double.infinity,
fit: BoxFit.cover,
),
),
cardController: controller,
swipeUpdateCallback: (DragUpdateDetails details, Alignment align) {
/// Get swiping card's alignment
if (align.x < 0) {
} else if (align.x > 0) {}
},
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
print(orientation.toString());
if (orientation == CardSwipeOrientation.LEFT) {
print("Card is LEFT swiping");
// print(welcomeImages.length);
onLeftPress();
} else if (orientation == CardSwipeOrientation.RIGHT) {
print("Card is RIGHT swiping");
// print(welcomeImages.length);
onRightPress();
}
},
),
);
}
}
Edited
Try with setstate
import 'package:flutter/material.dart';
import 'package:flutter_tindercard/flutter_tindercard.dart';
class ExampleHomePage extends StatefulWidget {
#override
_ExampleHomePageState createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage>
with TickerProviderStateMixin {
List approved = [];
List refused = [];
List<String> welcomeImages = [
"https://images.unsplash.com/photo-1631636176993-759dea1a1300?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw0fHx8ZW58MHx8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631634176568-f543af6a41de?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw3fHx8ZW58MHx8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631691971564-adf9419d904e?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwxM3x8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631641906574-24adb8594649?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwxMnx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1593642702821-c8da6771f0c6?ixid=MnwxMjA3fDF8MHxlZGl0b3JpYWwtZmVlZHwxMXx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
"https://images.unsplash.com/photo-1631621461675-e61a1f28d3d6?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw0NHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60"
];
List<TinderCarder> cards = [];
CardController controller = CardController(); //Use this to trigger swap.
#override
void initState() {
// TODO: implement initState
super.initState();
cards = welcomeImages.map((element) {
return TinderCarder(
image: element,
controller: controller,
onLeftPress: () {
setState(() {
cards.removeAt(element.indexOf(element));
print('index is: ' + "${element.indexOf(element)}");
print('list length: ' + cards.length.toString());
});
},
onRightPress: () {
setState(() {
cards.removeAt(element.indexOf(element));
print('index is: ' + "${element.indexOf(element)}");
print('list length: ' + cards.length.toString());
});
},
);
}).toList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
child: SingleChildScrollView(
child: ListView(
shrinkWrap: true,
key: Key(cards.length.toString()),
physics: ScrollPhysics(),
children: cards,
),
),
),
);
}
}
class TinderCarder extends StatelessWidget {
final String image;
Function onLeftPress;
Function onRightPress;
var controller;
TinderCarder(
{ this.image,
this.controller,
this.onLeftPress,
this.onRightPress});
#override
Widget build(BuildContext context) {
return Container(
height: 200,
child: TinderSwapCard(
orientation: AmassOrientation.BOTTOM,
totalNum: 1,
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) => Card(
child: Image.network(
image,
width: double.infinity,
fit: BoxFit.cover,
),
),
cardController: controller,
swipeUpdateCallback: (DragUpdateDetails details, Alignment align) {
/// Get swiping card's alignment
if (align.x < 0) {
} else if (align.x > 0) {}
},
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
print(orientation.toString());
if (orientation == CardSwipeOrientation.LEFT) {
print("Card is LEFT swiping");
// print(welcomeImages.length);
onLeftPress();
} else if (orientation == CardSwipeOrientation.RIGHT) {
print("Card is RIGHT swiping");
// print(welcomeImages.length);
onRightPress();
}
},
),
);
}
}
If you are using stateful widget then apply setstate() to build again your widget.
If you want to use stateless widget than use state management like GetX or provider.
For GetX create observable list in controller and wrap widget with obx so whenever list is updated the obx will build that particular widget for you again.
To Learn GetX Properly go through this playlist created by tadas petra.
https://www.youtube.com/playlist?list=PL26uY6-lIzqkmvpNr9gMCrIvl8k5Mqrhs
Im trying to listen to scroll for lazy loading but im not getting any Listen values,I have used ListView.builder widget and have attached a scroll contoller (_controller) and have instantiated controller in initState() method, need help with the issue
class _FeedsState extends State<Feeds> {
ScrollController _controller;
int pageNumber = 1;
#override
void initState() {
super.initState();
_controller = new ScrollController();
_controller.addListener(scrollListener);
SchedulerBinding.instance.addPostFrameCallback((_) {
_controller.animateTo(
0.0,
duration: const Duration(milliseconds: 10),
curve: Curves.easeOut,
);
});
}
#override
Widget build(BuildContext context) {
print(widget.feedDetails);
return widget.feedDetails.length == 0
? PostSomething(
isAdmin: widget.isAdmin,
)
: ListView.builder(
shrinkWrap: true,
controller: _controller,
itemBuilder: (context, index) {
return Column(
children: [
index == 0
? PostSomething(
isAdmin: widget.isAdmin,
profilePic: widget.feedDetails[0]["profile_pic"])
: Container(),
(Posts(
index: index,
feedDetails: widget.feedDetails[index],
displayProfileNavigation: widget.displayProfileNavigation,
)),
],
);
},
itemCount: widget.feedDetails.length,
);
}
void scrollListener() {
print("Scrolling");
if (_controller.position.pixels == _controller.position.maxScrollExtent) {
print("Coooool");
}
}
}
make: shrinkWrap: false,, this will enable your scrolling, if this show unbounded height exception, then try
return Scaffold(
body: Expanded(
ListView.builder(....your code..
I want to create a GridView that displays items which will be fetching from server by offset. I load only 10 items in GridView, and after user scroll reached to 10, I will load more 10 items. What are the best practices for implementing an infinite scroll GridView in Flutter?
You need to add the ScrollController for the scrolling detection at the bottom for the ListView and GridView. As you need the GridView i have created the ScrollController listner and added to the GridView's contollerfor the detection of the scroll. I have created the demo of it , please check it once. At first time it load the 10 items and when list comes to the bottom then it add more 10 items in it.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class HomeScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return HomeState();
}
}
class HomeState extends State<HomeScreen> {
List dataList = new List<int>();
bool isLoading = false;
int pageCount = 1;
ScrollController _scrollController;
#override
void initState() {
super.initState();
////LOADING FIRST DATA
addItemIntoLisT(1);
_scrollController = new ScrollController(initialScrollOffset: 5.0)
..addListener(_scrollListener);
}
Widget build(BuildContext context) {
return MaterialApp(
title: 'Gridview',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.red,
accentColor: Color(0xFFFEF9EB),
),
home: Scaffold(
appBar: new AppBar(),
body: GridView.count(
controller: _scrollController,
scrollDirection: Axis.vertical,
crossAxisCount: 2,
mainAxisSpacing: 10.0,
physics: const AlwaysScrollableScrollPhysics(),
children: dataList.map((value) {
return Container(
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.2,
margin: EdgeInsets.only(left: 10.0, right: 10.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
),
child: Text("Item ${value}"),
);
}).toList(),
)));
}
//// ADDING THE SCROLL LISTINER
_scrollListener() {
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
!_scrollController.position.outOfRange) {
setState(() {
print("comes to bottom $isLoading");
isLoading = true;
if (isLoading) {
print("RUNNING LOAD MORE");
pageCount = pageCount + 1;
addItemIntoLisT(pageCount);
}
});
}
}
////ADDING DATA INTO ARRAYLIST
void addItemIntoLisT(var pageCount) {
for (int i = (pageCount * 10) - 10; i < pageCount * 10; i++) {
dataList.add(i);
isLoading = false;
}
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
And output of the above program as follow
class AllOrdersPage extends StatefulWidget {
#override
_AllOrdersPageState createState() => _AllOrdersPageState();
}
class _AllOrdersPageState extends State<AllOrdersPage> {
List<OrderDatum> ordersList;
ScrollController _scrollController = ScrollController();
int skip = 0;
bool shouldLoadMore = true;
Future<OrdersResponse> future;
#override
void initState() {
super.initState();
ordersList = [];
future = getAllOrders(skip); //load data for first time
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) { //Check whether user scrolled to last position
if (shouldLoadMore) {
setState(() {
skip += ordersList.length;
future = getAllOrders(skip); //load more data
});
}
}
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<OrdersResponse>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasError)
return ErrorText('${snapshot.error.toString()}');
if (snapshot.hasData) {
skip = snapshot.data.skip;
if (snapshot.data.limit + snapshot.data.skip >=
snapshot.data.total) {
shouldLoadMore = false;
}
snapshot.data.data.forEach((element) {
if (!ordersList.contains(element)) ordersList.add(element);
});
if (skip == 0 && ordersList.isEmpty) {
return ErrorText('No orders.');
}
return Scrollbar(
controller: _scrollController,
isAlwaysShown: true,
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
childAspectRatio: 2.5,
mainAxisSpacing: 8),
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemBuilder: (BuildContext context, int index) {
if (index == ordersList.length) {
return shouldLoadMore
? Center(child: CircularProgressIndicator())
: Container();
}
return Container(
width: MediaQuery.of(context).size.width,
child: OrderCard(ordersList[index]));
},
itemCount: ordersList.length + 1,
),
);
}
return Loader();
});
}
}
Thanks
I want to implement pagination in GridView I use GridView.builder I want to download 10 by 10 items when the user reaches the last row
You can do this using a NotificationListener. As a simple demonstration it will increase the length of your GridView whenever it reaches end of page :
var items_number = 10 ;
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification){
if(scrollNotification.metrics.pixels == scrollNotification.metrics.maxScrollExtent){
setState(() {
items_number += 10 ;
});
}
},
child: GridView.builder(
itemCount: items_number,
itemBuilder: (context, index) {
//.... the reminder of your code
}
),
);
I also needed this but couldn't find any widget for the gridview pagination, so I tried to make a component based on #Mazin Ibrahim's answer below. It seems to be working but not sure if it is the right way to do this.
typedef Future<bool> OnNextPage(int nextPage);
class GridViewPagination extends StatefulWidget {
final int itemCount;
final double childAspectRatio;
final OnNextPage onNextPage;
final Function(BuildContext context, int position) itemBuilder;
final Widget Function(BuildContext context) progressBuilder;
GridViewPagination({
this.itemCount,
this.childAspectRatio,
this.itemBuilder,
this.onNextPage,
this.progressBuilder,
});
#override
_GridViewPaginationState createState() => _GridViewPaginationState();
}
class _GridViewPaginationState extends State<GridViewPagination> {
int currentPage = 1;
bool isLoading = false;
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification sn) {
if (!isLoading && sn is ScrollUpdateNotification && sn.metrics.pixels == sn.metrics.maxScrollExtent) {
setState(() {
this.isLoading = true;
});
widget.onNextPage?.call(currentPage++)?.then((bool isLoaded) {
setState(() {
this.isLoading = false;
});
});
}
return true;
},
child: CustomScrollView(
slivers: <Widget>[
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 1,
mainAxisSpacing: 1,
crossAxisCount: 2,
childAspectRatio: widget.childAspectRatio,
),
delegate: SliverChildBuilderDelegate(
widget.itemBuilder,
childCount: widget.itemCount,
addAutomaticKeepAlives: true,
addRepaintBoundaries: true,
addSemanticIndexes: true,
),
),
if (isLoading)
SliverToBoxAdapter(
child: widget.progressBuilder?.call(context) ?? _defaultLoading(),
),
],
),
);
}
Widget _defaultLoading() {
return Container(
padding: EdgeInsets.all(15),
alignment: Alignment.center,
child: CircularProgressIndicator(),
);
}
}
Example -
GridViewPagination(
itemCount: 10,
childAspectRatio: 1,
itemBuilder: _buildGridItem,
onNextPage: (int nextPage) {
return fetchData();
},
)
create a Scroll controller
ScrollController _scrollController = new ScrollController();
add a scroll event listener
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// Bottom poistion
}
});
}
Now just need to set the controller in your GridView, ListViewand ...
GridView.builder(
controller: _scrollController,
));
You can use this plugin here: Paging. Wrap your GridView inside of it and tell me if this works!
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.