AnimatedList auto scrolling to last item and back - flutter

I've implemented an Animated list with SlideTransition like this
#override
Widget build(BuildContext context) {
return Expanded(
child: Container(
child: ListView(
children: [
// Other widgets
animatedList(),
],
),
),
);
}
Widget animatedList() {
return AnimatedList(
shrinkWrap: true,
key: _myKeyList,
initialItemCount: _myItemsList.length,
itemBuilder: (context, index, animation) {
return SlideTransition(
position: animation.drive(_offset),
child: _buildMyItemTile[index],
);
},
);
}
where _offset variable is a Tween animation. Each item of list is inserted and animated with a delay of 500 milliseconds.
Now, when all items are added to AnimatedList, i would like that AnimatedList content scroll automatically from first item to last (and back) continuously for show all its content.
How can i do?

Just do it
add a controller
final controll = ScrollController();
then add this controller in the AnimatedList
AnimatedList(
controller: controll,
...
)
Make a function to call the last position or the first
0 for first position and 1 for last position or true false, I don't know, use your imagination
void getLastItem(int 0){
var position = int == 0
? controll.position.minScrollExtent
: controll.position.maxScrollExtent;
controll.animateTo(
position,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInCubic,
)
}
I hope I helped you.

Related

How to create a ListView scrolled to its top?

I'm a very novice Flutter programmer. I meet the following problem: the code shown below when recreates the ListView after changing data in the stream remembers the previous scroll position whereas I would like to scroll the list to its top. How could I do that?
class HintViewState extends State<HintView> {
#override
Widget build(BuildContext context) {
return StreamBuilder<List<String>>(
stream: model.hintsStream.stream,
initialData: [],
builder: (
BuildContext context,
AsyncSnapshot<List<String>> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active ||
snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Eraro dum prenado de sugestoj!');
} else if (snapshot.hasData) {
return Expanded(
child: ListView(
padding: const EdgeInsets.all(0.0),
scrollDirection: Axis.vertical,
children: snapshot.data!.map((word) => Padding(...)).toList(),
),
);
} else {
return const Text("Ne estas sugestoj!");
}
} else {
return Text('Stato: ${snapshot.connectionState}');
}
},
);
}
}
Create a scroll controller like as below
final ScrollController _scrollController = ScrollController();
Set it to the list view like as below
child: ListView.builder(
controller: _scrollController,
.....
When you want to scroll it to top, please add below code.
_scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn);
In your example, you can do like,
else if (snapshot.hasData) {
_scrolltoTop();
return Expanded(
child: ListView(
padding: const EdgeInsets.all(0.0),
scrollDirection: Axis.vertical,
children: snapshot.data!.map((word) => Padding(...)).toList(),
),
);
}
void _scrolltoTop(){
Future.delayed(const Duration(milliseconds: 1000), () {
_scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn);
});
}
In a Flutter community, they explained the situation to me. And the answer is the following.
The key thing here is the parameter "key" of a widget (not a bad pun, yeah?). When during rebuilding at some place in the UI tree, a widget appears of the same type and with the same key as the previous one, then this widget is not recreated, but modified only. And in my case, evidently, modification of a ListView does not imply scrolling to a new position. "That's not a bug, that's a feature!".
One way to force complete recreation a widget when new data appear in the stream is to pass a new key. The best way to get a new key is to use the function ValueKey(...), which, evidently, computes some hash of the object passed to it and returns a key based on the hash. So, when the function is used with the data to be shown, a change of the data in the stream leads to a change of the key and, therefore, to a complete recreation of the widget, which in the case of a ListView assumes scrolling to the top.
So, the correction of the code is the following:
..............
return Expanded(
child: ListView(
key: ValueKey(snapshot.data),
padding: const EdgeInsets.all(0.0),
scrollDirection: Axis.vertical,
..............
But I'm not sure if in a general case, this widget only is recreated or its entire subtree. If the latter, then it might be costly.

PageView rebuilding while animateToPage is in progress

I'm creating a social media feed where each post is an image of a different size. The user can swipe right to like, left to dislike, up to skip to the next post, or down to go back. To do that, I'm using a Dismissible widget within a PageView, where each page contains a post/image. I used "animateToPage" in the Dismissible to automatically animate to the next page once the user swipes right or left.
The problem is that when the PageView animates to the next page, the image that was dismissed suddenly reappears on the previous page while the animation is happening. I want it to reappear only if the user swipes down to go back to the previous post, but not while the PageView is animating.
Here's a video showing what is going wrong
And here's an animation showing what I need
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin{
int pageIndex = 0;
PageController _pageController = PageController(
initialPage: 0,
);
#override
Widget build(BuildContext context) {
List images = [
'assets/1.jpg', 'assets/2.jpg', 'assets/3.jpg', 'assets/4.jpg', 'assets/5.jpg',
];
return MaterialApp(
home: Scaffold(
backgroundColor: Color.fromRGBO(250, 250, 250, 1),
body: LayoutBuilder(
builder: (context, constraints) => PageView.builder(
controller: _pageController,
itemCount: 5,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return images.map((image) => Dismissible(
onResize: () {
setState(() {
_pageController.animateToPage(index+1, duration: Duration(milliseconds: 300), curve: Curves.ease);
});
},
onDismissed: (direction) {},
key: UniqueKey(),
child: Container(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 100),
child: Container(
alignment: Alignment.center,
child: Image(
image: AssetImage(image)
),
),
),
),
),
))
.toList()[index];
}
),
),
),
);
}
}
I assume this is happening because PageView is rebuilding the other pages while the animation is in progress. I'm still a beginner in Flutter and wasn't able to find a solution. Any ideas of how to fix this?
Everytime setState is called, the widget is redrawn. Try to put your animated code outside of setState method.
Documentation

Flutter : Horizontal Listview with viewportFraction

I want to implement this horizontal ListView effect.
Planning to create a horizontal ListView like this. Example the horizontal listView will show 2 items and 1 item only show up 20%.
When scrolling it will become like this. Example front and end show up 20% and center show 2 items.
Edited : Code I'm using right now :
viewportFraction = 1 / 2.3;
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double itemWidth =
(constraints.maxWidth - padding.horizontal) * this.viewportFraction;
final double itemHeight = (itemWidth * this.aspectRatio);
return new Container(
height: itemHeight,
child: new ListView.custom(
scrollDirection: Axis.horizontal,
controller: new PageController(
initialPage: this.initialPage,
viewportFraction: this.viewportFraction,
),
physics: const PageScrollPhysics(),
padding: this.padding,
itemExtent: itemWidth,
childrenDelegate: this.childrenDelegate,
),
);
});
The easiest way of doing this is probably with an horizontal ListView builder
#override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection:Axis.horizontal,
itemCount:10,
itemBuilder:(context,index){
return Padding(
padding:EdgeInsets.symmetric(horizontal:MediaQuery.of(context).size.width*0.05),
child:Container(
color: Colors.blue,
child:Text(index.toString()),
height:20,
width:MediaQuery.of(context).size.width*0.25,
)
);
}
);
}
The MediaQuery.of(context).size allows you to get information about your sreen geometry
I'll let you do some math to find the right fraction of the screen to use to get the final result that you want

Flutter PageView, can i animate removing items from list?

I'm pretty new to flutter and i'm trying to do some animation on a PageView. to be precise, I want to animate removing an item.
I've tried serveral ways to animate it and apart from a solution, the way how you guys would solve such a problem would also be helpful for my flutter skils.
What I've tried so far:
Animating the padding and opacity
the problem with this is that when i set the padding in the setState in the onLongPress it rebuilds the widget and it overrides the padding again with the active or inactive CardPadding (i think)
Animating the width and height
I just can't seem to get both of these values to work
Animating the viewportFraction on the PageViewController
Would not know how to go about this and if it would be possible to do this only for a specific 'Page'
Below is the (stripped down) code I've written thus far.
class Main extends StatefulWidget {
#override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
int activeCard = 0;
EdgeInsets inActiveCardPadding = EdgeInsets.symmetric(vertical: 120.0, horizontal: 20.0);
EdgeInsets activeCardPadding = EdgeInsets.symmetric(vertical: 105.0, horizontal: 10.0);
PageController pageController = PageController(
initialPage: 0,
viewportFraction: 0.8,
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: <Widget>[
PageView.builder(
itemCount: PlantCareApp.plants.length,
controller: pageController,
onPageChanged: (activeCardIndex) {
setState(() {
this.activeCard = activeCardIndex;
});
},
itemBuilder: (context, cardIndex) {
return AnimatedContainer(
padding: (activeCard == cardIndex) ? activeCardPadding : inActiveCardPadding;,
duration: Duration(milliseconds: 250),
child: PlantCard(
PlantCareApp.plants[cardIndex],
onTap: () {
Navigator.pushNamed(context, PlantDetailScreen.route, arguments: PlantCareApp.plants[cardIndex]);
},
onLongPress: () {
setState(() {
//
// ANIMATE OR TRIGGER ANIMATION HERE
//
// do the actual removing
/*
PlantCareApp.plants[cardIndex].remove(); // remove from db
PlantCareApp.plants.removeAt(cardIndex); // remove from List
*/
});
//PlantCareApp.plants[cardIndex].remove();
},
),
);
},
),
],
),
),
);
}
}
Any help will be greatly appreciated! How would you guys tackle a problem like this, or how would you tackle this specific use case.
I guess actually animating viewportFraction would be the nicest because of the adjecent 'Pages' moving toward each other as well?
Thanks!
I'm not certain if this is what you are looking for, but here goes.
One way of doing this is simply using the provided Widgets within Flutter. Two of these will help you out: AnimatedList and Dismissible.
Now, you could do something like this:
// define somewhere
final _animatedListGK = GlobalKey<AnimatedListState>();
// put in a function somewhere
return AnimatedList(
key: _animatedListGK,
padding: const EdgeInsets.all(0),
initialItemCount: PlantCareApp.plants.length,
itemBuilder: (context, index, animation) {
return FadeTransition(
opacity: animation,
child: _buildDismissibleRow(context, index, PlantCareApp.plants[index])
);
}
);
Note: you don't have to use the _animatedListGK global key per se, it depends on whether you can use AnimatedList.of(context) or not. Although it is the easier way.
The _animatedListGK is simply a Global Key that provides access to the AnimatedList so you can perform insertions/removals with animation.
Your dismissible row might look something like:
Widget _buildDismissibleRow(BuildContext context, int index, PlantModel plantModel) {
return Dismissible(
key: ValueKey<String>(plantModel.someKey),
direction: DismissDirection.startToEnd,
background: Container(color: Colors.red),
onDismissed: (direction) {
// You could use:
// AnimatedList.of(context)
_animatedListGK.currentState.removeItem(
index,
(context, animation) => Container(),
duration: Duration.zero
);
},
child: _buildContent(context, index, plantModel)
);
}
You could also do it without a dismissible row or even within the child of the dismissible row (_buildContent() for example). Something similar to:
// You could use:
// AnimatedList.of(context)
_animatedListGK.currentState.removeItem(
index,
(context, animation) {
return FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Interval(0.5, 1.0)),
child: SizeTransition(
sizeFactor: CurvedAnimation(parent: animation, curve: Interval(0.0, 1.0)),
child: _builContent(context, index, plantModel)
)
);
},
duration: const Duration(milliseconds: 300)
);
Notice how the SizeTransition simply "calls itself" by calling _builContent(context, index, plantModel)? That's how you can animate the row itself (out of existence).
Be sure to watch the videos in the aforementioned documentation pages! They will help understanding certain constructs.
A preview of what the dismissible might look like:
A preview of what the SizedTransition might look like:

Listview Scrolling to widget with variable height

I have a Listview filled with Text Widgets of variable height:
ListView.builder(
controller: _controller,
itemCount: items.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(items[index].text));
})
I use a ScrollController (_controller) to animate the list and scroll down when the user clicks a button.
_moveDown() {
_controller.animateTo(_controller.offset + 100,
curve: Curves.linear, duration: Duration(milliseconds: 100));
}
I have no issues scrolling in fixed increments (100px in the example above), but I can't figure out how to calculate the height of the widgets in the list so the user can scroll among the list items.
Is there a way to get the height of each of the elements of the list?
Yes, there is.
You need to give a Key, for example a GlobalKey to your child widget, which by your example, looks like a Padding, then you can find the height of the rendering widget. Your method to handle your scroll could then be something like this where myKey is each items key.
void _moveDown(GlobalKey myKey) {
final keyContext = myKey.currentContext;
if (keyContext != null) {
final box = keyContext.findRenderObject() as RenderBox;
_scrollController.animateTo(_scrollController.offset + box.size.height,
duration: Duration(milliseconds: 100), curve: Curves.linear);
}
}