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);
}
}
Related
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.
I created a design for a chat application, and tried to scroll to end automatically, I used this code
scrollController.animateTo(
scrollController.position.maxScrollExtent,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
);
nothing happens, ofc the list has the controller applied
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 10),
shrinkWrap: true,
controller: scrollController,
itemCount: chatMessage.length,
itemBuilder: (context, index) {
return ChatBubble(
chatMessage: chatMessage[index],
isDark: widget._isDark,
);
},
)
I tried to print the maxScrollExtent to see if something was wrong.
Both the max and min scrollExtent are equals to 0.0.
The animateTo function is also call in a postFrameCallback.
The listview length is always 20 has, for now, it's a simple list.
For me, the issue was that my ListView was inside a ScrollView. Removing the ScrollView fixed the issue.
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
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:
I need to create an overlapping pageview collection, but because of draw/layout order of items the second page always shows up in front of first page. There a way to create a collection list that the first items overlapping the others?
PAGE BUILDER ->
Widget buildList(PreloadPageController pageController, List data,
double currentPosition) {
return AspectRatio(
aspectRatio: 12.0 / 15.0,
child: PreloadPageView.builder(
itemCount: data.length,
controller: pageController,
preloadPagesCount: 2,
itemBuilder: (context, index) {
return CardWidget(
page: index,
currentPage: currentPosition,
);
},
),
);
}
CARD WIDGET ->
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, contraints) {
final double padding = 20.0;
var delta = currentPage - page;
var start = padding * delta.abs() * 10;
var top = padding + padding * max(-delta, 0.0);
var bottom = padding + padding * max(-delta, 0.0);
//print(start);
return Transform.translate(
offset: Offset(-start, 0),
child: Container(
padding: EdgeInsets.only(top: top, bottom: bottom),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Container(
color: _randomColor(page),
),
),
),
);
});
}
I was expecting create a collection effect so the second page would come from behind the first one, but actually second pages always appears overlapping the first.
I could use reverse in PageView.builder, but this collection needs to be a infinity list that loads more data when it reaches the end and with reverse the code will be alot trickier.
I'm achieving this:
But what I want is the blue card behind the red one.
So right now, to create your overlapping effect, you are offsetting the next-Page so that it overlaps the cur-Page, and as you mentioned, you discover that the next-Page visually over-laps instead of the desired under-lap.
Then, in additional to your offset, have you tried cropping off the overlapping portion of next-Page? This can simulate the effect of under-lapping.
Now, I tried replicating your sample, but I'm uncertain about your PreloadPageController (and maybe other details), so my sample might look glitchy. Additionally, I'm not wholly familiar with cropping widgets. But I bumped into this possible solution, all I did was wrap it with another ClipRect:
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final double padding = 20.0;
var delta = widget.currentPage - widget.myPage;
var start = padding * delta.abs() * 10;
var top = padding + padding * max(-delta, 0.0);
var bottom = padding + padding * max(-delta, 0.0);
return ClipRect(
child: Transform.translate(
offset: Offset(-start, 0),
child: Container(
padding: EdgeInsets.only(top: top, bottom: bottom),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Container(
color: redOrBlue(widget.myPage),
),
),
),
),
);
});
}
This additional ClipRect basically clips off your offset portion. Feel free to explore and modify as needed!
There's an Overlay widget but it may cause more headaches, especially at scale. You can wrap your pages in a ClipRect, but only the widgets at the end of the list will need it. So in the builder function, set up a bool:
clipNeeded = (controller.page + 0.5) <= index;
Then in the new ClipRect widget:
clipper: clipNeeded ? CustomRect() : null,
Then you'll need to create that CustomRect class, extending CustomClipper<Rect>
The exact math required depends on the implementation, but in this left to right scroll example, it'll be something like
class CustomRect extends CustomClipper<Rect>{
#override
Rect getClip(Size size) {
double leftLine = /* some calculation */;
return Rect.fromLTRB(leftLine, 0.0, size.width, size.height);
}
#override
bool shouldReclip(CustomRect oldClipper) {
return true;
}
}