What I needed:
I want to scroll a list by some index, how can i do that.
What I know:
scrollToIndex should start from n index, but how can we scroll to any index?
Unfortunately, ListView has no built-in approach to a scrollToIndex() function. You’ll have to develop your own way to measure to that element’s offset for animateTo() or jumpTo(), or you can search through these suggested solutions/plugins or from other posts like Flutter: Scrolling to a widget in ListView
(the general scrollToIndex issue is discussed at flutter/issues/12319 since 2017, but still with no current plans)
But there is a different kind of ListView that does support scrollToIndex (as mentioned by Slashbin):
ScrollablePositionedList
dependency: scrollable_positioned_list
You set it up exactly like ListView and works the same, except you now have access to a ItemScrollController that does:
jumpTo({index, alignment})
scrollTo({index, alignment, duration, curve})
Simplified example:
ItemScrollController _scrollController = ItemScrollController();
ScrollablePositionedList.builder(
itemScrollController: _scrollController,
itemCount: _myList.length,
itemBuilder: (context, index) {
return _myList[index];
},
)
_scrollController.scrollTo(index: 150, duration: Duration(seconds: 1));
(note that this library is developed by Google but not by the core Flutter team.)
ScrollablePositionedList can be used for this.
https://github.com/google/flutter.widgets/tree/master/packages/scrollable_positioned_list
Pub link - https://pub.dev/packages/scrollable_positioned_list
final ItemScrollController itemScrollController = ItemScrollController();
final ItemPositionsListener itemPositionListener = ItemPositionsListener.create();
ScrollablePositionedList.builder(
itemCount: 500,
itemBuilder: (context, index) => Text('Item $index'),
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionListener,
);
One then can scroll to a particular item with:
itemScrollController.scrollTo(
index: 150,
duration: Duration(seconds: 2),
curve: Curves.easeInOutCubic);
Use scroll_to_index lib, here scroll will be always performed to sixth position as its hardcoded below
dependencies:
scroll_to_index: ^1.0.6
Code Snippet:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final scrollDirection = Axis.vertical;
AutoScrollController controller;
List<List<int>> randomList;
#override
void initState() {
super.initState();
controller = AutoScrollController(
viewportBoundaryGetter: () =>
Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
axis: scrollDirection);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
scrollDirection: scrollDirection,
controller: controller,
children: <Widget>[
...List.generate(20, (index) {
return AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
child: Container(
height: 100,
color: Colors.red,
margin: EdgeInsets.all(10),
child: Center(child: Text('index: $index')),
),
highlightColor: Colors.black.withOpacity(0.1),
);
}),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _scrollToIndex,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
// Scroll listview to the sixth item of list, scrollling is dependent on this number
Future _scrollToIndex() async {
await controller.scrollToIndex(6, preferPosition: AutoScrollPosition.begin);
}
}
Output:
It's simple to scroll to preferred index if you know listview item size like:
var index = 15;
var widthOfItem =
176; //in dp. width needed for horizontallistView;
var heightOfItem =
200; //in dp. height need for vertical listView;
listViewScrollController.jumpTo(index * widthOfItem.toDouble());
or
listViewScrollController.animateTo(
index * widthOfItem.toDouble(),
duration: Duration(milliseconds: 500),
curve: Curves.ease);
Found a gist and its working gist url is https://gist.github.com/debuggerx01/b8ef756ee02b3eb82ec3702f14ba94e6
this gist is using a package which is calculation rect size.
https://pub.dartlang.org/packages/rect_getter
You can use the flutter_scrollview_observer lib to implement your desired functionality without invasivity
Create and use instance of ScrollController normally.
ScrollController scrollController = ScrollController();
ListView _buildListView() {
return ListView.separated(
controller: scrollController,
...
);
}
Create an instance of ListObserverController pass it to ListViewObserver
ListObserverController observerController = ListObserverController(controller: scrollController);
ListViewObserver(
controller: observerController,
child: _buildListView(),
...
)
Now you can scroll to the specified index position
// Jump to the specified index position without animation.
observerController.jumpTo(index: 1)
// Jump to the specified index position with animation.
observerController.animateTo(
index: 1,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
Related
I'm making an image gallery and I need the user to be able to long-press an image to show a popup menu which will let him delete the image.
My code, so far:
return GestureDetector(
onLongPress: () {
showMenu(
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: Image.memory(
this._asset.thumbData.buffer.asUint8List(),
fit: BoxFit.cover,
gaplessPlayback: true,
),
);
Which produces:
But also, I couldn't find out how to completely remove the image's widget when the longPress function is called. How to do so?
The OP and the First Answerer bypassed the original problem using PopupMenuButton, which worked fine in their case. But I think the more general question of how to position one's own menu and how to receive the user's response without using PopupMenuButton is worth answering, because sometimes we want a popup menu on a custom widget, and we want it to appear on some gestures other than a simple tap (e.g. the OP's original intention was to long-press).
I set out to make a simple app demonstrating the following:
Use a GestureDetector to capture long-press
Use the function showMenu() to display a popup menu, and position it near the finger's touch
How to receive the user's selection
(Bonus) How to make a PopupMenuEntry that represents multiple values (the oft-used PopupMenuItem can only represent a single value)
The result is, when you long-press on a big yellow area, a popup menu appears on which you can select +1 or -1, and the big number would increment or decrement accordingly:
Skip to the end for the entire body of code. Comments are sprinkled in there to explain what I am doing. Here are a few things to note:
showMenu()'s position parameter takes some effort to understand. It's a RelativeRect, which represents how a smaller rect is positioned inside a bigger rect. In our case, the bigger rect is the entire screen, the smaller rect is the area of touch. Flutter positions the popup menu according to these rules (in plain English):
if the smaller rect leans toward the left half of the bigger rect, the popup menu would align with the smaller rect's left edge
if the smaller rect leans toward the right half of the bigger rect, the popup menu would align with the smaller rect's right edge
if the smaller rect is in the middle, which edge wins depends on the language's text direction. Left edge wins if using English and other left-to-right languages, right edge wins otherwise.
It's always useful to reference PopupMenuButton's official implementation to see how it uses showMenu() to display the menu.
showMenu() returns a Future. Use Future.then() to register a callback to handle user selection. Another option is to use await.
Remember that PopupMenuEntry is a (subclass of) StatefulWidget. You can layout any number of sub-widgets inside it. This is how you represent multiple values in a PopupMenuEntry. If you want it to represent two values, just make it contain two buttons, however you want to lay them out.
To close the popup menu, use Navigator.pop(). Flutter treats popup menus like a smaller "page". When we display a popup menu, we are actually pushing a "page" to the navigator's stack. To close a popup menu, we pop it from the stack, thus completing the aforementioned Future.
Here is the full code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _count = 0;
var _tapPosition;
void _showCustomMenu() {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
position: RelativeRect.fromRect(
_tapPosition & const Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
)
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: _storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(
fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
If you are going to use a gridView or listview for laying out the images on the screen, you can wrap each item with a gesture detector then you should keep your images in a list somewhere, then simply remove the image from the list and call setState().
Something like the following. (This code will probably won't compile but it should give you the idea)
ListView.builder(
itemCount: imageList.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onLongPress: () {
showMenu(
onSelected: () => setState(() => imageList.remove(index))}
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: imageList[index],
);
}
)
Edit: You can use a popup menu too, like following
Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
width: 100,
child: PopupMenuButton(
child: FlutterLogo(),
itemBuilder: (context) {
return <PopupMenuItem>[new PopupMenuItem(child: Text('Delete'))];
},
),
),
Building on the answers by Nick Lee and hacker1024, but instead of turning the solution into a mixin, you could simply just turn it into a widget:
class PopupMenuContainer<T> extends StatefulWidget {
final Widget child;
final List<PopupMenuEntry<T>> items;
final void Function(T) onItemSelected;
PopupMenuContainer({#required this.child, #required this.items, #required this.onItemSelected, Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => PopupMenuContainerState<T>();
}
class PopupMenuContainerState<T> extends State<PopupMenuContainer<T>>{
Offset _tapDownPosition;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details){
_tapDownPosition = details.globalPosition;
},
onLongPress: () async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
T value = await showMenu<T>(
context: context,
items: widget.items,
position: RelativeRect.fromLTRB(
_tapDownPosition.dx,
_tapDownPosition.dy,
overlay.size.width - _tapDownPosition.dx,
overlay.size.height - _tapDownPosition.dy,
),
);
widget.onItemSelected(value);
},
child: widget.child
);
}
}
And then you'd use it like this:
child: PopupMenuContainer<String>(
child: Image.asset('assets/image.png'),
items: [
PopupMenuItem(value: 'delete', child: Text('Delete'))
],
onItemSelected: (value) async {
if( value == 'delete' ){
await showDialog(context: context, child: AlertDialog(
title: Text('Delete image'),
content: Text('Are you sure you want to delete the image?'),
actions: [
uiFlatButton(child: Text('NO'), onTap: (){ Navigator.of(context).pop(false); }),
uiFlatButton(child: Text('YES'), onTap: (){ Navigator.of(context).pop(true); }),
],
));
}
},
),
Adjust the code to fit your needs.
Nick Lee's answer can be turned into a mixin quite easily, which can then be used anywhere you want to use a popup menu.
The mixin:
import 'package:flutter/material.dart' hide showMenu;
import 'package:flutter/material.dart' as material show showMenu;
/// A mixin to provide convenience methods to record a tap position and show a popup menu.
mixin CustomPopupMenu<T extends StatefulWidget> on State<T> {
Offset _tapPosition;
/// Pass this method to an onTapDown parameter to record the tap position.
void storePosition(TapDownDetails details) => _tapPosition = details.globalPosition;
/// Use this method to show the menu.
Future<T> showMenu<T>({
#required BuildContext context,
#required List<PopupMenuEntry<T>> items,
T initialValue,
double elevation,
String semanticLabel,
ShapeBorder shape,
Color color,
bool captureInheritedThemes = true,
bool useRootNavigator = false,
}) {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
return material.showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
_tapPosition.dx,
_tapPosition.dy,
overlay.size.width - _tapPosition.dx,
overlay.size.height - _tapPosition.dy,
),
items: items,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
shape: shape,
color: color,
captureInheritedThemes: captureInheritedThemes,
useRootNavigator: useRootNavigator,
);
}
}
And then, to use it:
import 'package:flutter/material.dart';
import './custom_context_menu.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with CustomPopupMenu {
var _count = 0;
void _showCustomMenu() {
this.showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
Answer for 2023
In Flutter 3.7 there is now a ContextMenuRegion widget that you can wrap around any existing widget. When the user long presses or right-clicks (depending on the platform), the menu you give it will appear.
return Scaffold(
body: Center(
child: ContextMenuRegion(
contextMenuBuilder: (context, offset) {
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: TextSelectionToolbarAnchors(
primaryAnchor: offset,
),
buttonItems: <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {
ContextMenuController.removeAny();
},
label: 'Save',
),
],
);
},
child: const SizedBox(
width: 200.0,
height: 200.0,
child: FlutterLogo(),
),
),
),
);
I'm looking for a way to insert new items into a list view, while maintaining the scroll offset of the user. Basically like a twitter feed after pulling to refresh: the new items get added on top, while the scroll position is maintained. The user can then just scroll up to see the newly added items.
If I just rebuild the list/scroll widget with a couple of new items at the beginning, it -of course- jumps, because the height of the scroll view content increased. Just estimating the height of those new items to correct the jump is not an option, because the content of the new items is variable.
Even the AnimatedList widget which provides methods to dynamically insert items at an arbitrary position jumps when inserting at index 0.
Any ideas on how to approach this? Perhaps calculating the height of the new items beforehand using an Offstage widget?
Ran into this problem recently: I have a chat scroll that async loads previous or next messages depending on the direction of the scroll. This solution worked out for me.
The idea of the solution is the following. You create two SliverLists and put them inside CustomScrollView.
CustomScrollView(
center: centerKey,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the upper group
child: top[index]
)
}
),
SliverList(
// Key parameter makes this list grow bottom
key: centerKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the bottom group
child: bottom[index]
)
}
),
)
The first list scrolls upwards while the second list scrolls downwards. Their offset zero points are fixed at the same point and never move. If you need to prepend an item you push it to the top list, otherwise, you push it to the bottom list. That way their offsets don't change and your scroll view does not jump.
You can find the solution prototype in the following dartpad example.
UPD: Fixed null safety issue in this dartpad example.
I don't know if you managed to solve it... Marcin Szalek has posted a very nice solution on his blog about implementing an infinite dynamic list. I tried it and works like a charm with a ListView. I then tried to do it with an AnimatedList, but experienced the same issue that you reported (jumping to the top after each refresh...). Anyway, a ListView is quite powerful and should do the trick for you!
The code is:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(primarySwatch: Colors.blue),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<int> items = List.generate(10, (i) => i);
ScrollController _scrollController = new ScrollController();
bool isPerformingRequest = false;
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
_getMoreData() async {
if (!isPerformingRequest) {
setState(() => isPerformingRequest = true);
List<int> newEntries = await fakeRequest(
items.length, items.length + 10); //returns empty list
if (newEntries.isEmpty) {
double edge = 50.0;
double offsetFromBottom = _scrollController.position.maxScrollExtent -
_scrollController.position.pixels;
if (offsetFromBottom < edge) {
_scrollController.animateTo(
_scrollController.offset - (edge - offsetFromBottom),
duration: new Duration(milliseconds: 500),
curve: Curves.easeOut);
}
}
setState(() {
items.addAll(newEntries);
isPerformingRequest = false;
});
}
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isPerformingRequest ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Infinite ListView"),
),
body: ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return _buildProgressIndicator();
} else {
return ListTile(title: new Text("Number $index"));
}
},
controller: _scrollController,
),
);
}
}
/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
return Future.delayed(Duration(seconds: 2), () {
return List.generate(to - from, (i) => i + from);
});
}
A gist containing whole class can be found here.
I think reverse + lazyLoading will help you.
Reverse a list:
ListView.builder(reverse: true, ...);
for lazyLoading refer here.
There are two options to retrieve the scroll position for a CustomScrollView. The documentation states the following:
ScrollNotification and NotificationListener, which can be used to watch the scroll position without using a ScrollController.
So we have the following options:
NotificationListener with ScrollNotification
ScrollController
In which situation do you useNotificationListener with ScrollNotification vs ScrollController?
Thank you :)
If you're using NestedScrollView with nested scrollers, using a scrollController on the inner scrollers will break the link with NestedScrollView meaning NestedScrollView will no longer control the complete scrolling experience. To get information about the scroll positions of the inner scrollers in this case you would use a NotificationListener with ScrollNotification.
NotificationListener<ScrollNotification>(
child: ListView.builder(
itemCount: 10
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels ==
scrollInfo.metrics.maxScrollExtent) {
onLoadMore();
}
},
);
Related Answer here.
Although you can use both ScrollNotification and ScrollController to listen scroll position changes, and many times it is not going to be difference, you should keep in mind some details in order to better choose the best tool for the job:
ScrollController
To use a ScrollController you should use a Stateful widget, so that it can be properly dispose.
Even if you can use the same ScrollController to control several scrollable widgets, some operations, such as reading the scroll offset, require the controller to be used with a single scrollable widget.
If you want to change the scroll position, you are going to need a ScrollController (jumpTo / animateTo).
When you listen to a ScrollController, you are listening to ScrollPosition changes, what is not exactly the same as ScrollNotification. Using ScrollNotifications you can differentiate among several types of events related to the scrolling action, like scroll start, scroll end, dirección change, etc.
NotificationListener
NotificationListener is just another widget, so yo don't need to create a Stateful widget.
It listens ScrollNotifications from all scrollable widgets below in the widget tree, as notifications propagate upwards. So it is not constraint to listen only one widget.
You cannot change the scroll position from a NotificationListener. It is read only.
You listen to ScrollNotification, and not to changes in a ScrollPosition, so you can recognize easily which king of event has just happened.
it my demo use NotificationListener with ScrollController. After drag a litter left, the blue part will automatic move
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
List<Widget> list = [];
for (int i = 0; i < 100; i++) list.add(buildContainer());
return Scaffold(
body: ListView(children: list));
}
Widget buildContainer() {
ScrollController _scrollController = ScrollController();
return NotificationListener<ScrollNotification>(
onNotification: (scrollState) {
if (scrollState is ScrollEndNotification && scrollState.metrics.pixels != 160) {
Future.delayed(const Duration(milliseconds: 100), () {}).then((s) {
_scrollController.animateTo(160,
duration: Duration(milliseconds: 500), curve: Curves.ease);
});
}
return false;
},
child: Container(
height: 160,
margin: EdgeInsets.only(bottom: 1),
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
controller: _scrollController,
children: <Widget>[
Container(
width: 360,
height: 20,
color: Colors.red,
),
Container(
width: 160,
height: 20,
color: Colors.blue,
),
],
),
),
);
}
}
I'm looking to animate the scale of a container using the transform property of an AnimatedContainer; however, the scale is not being transitioned and jumps directly from start to end.
Code Snippet:
var container = new AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 50.0,
height: 50.0,
// selected is a bool that will be toggled
transform: selected ? new Matrix4.identity() : new Matrix4.identity().scaled(0.2,0.2),
decoration: new BoxDecoration(
shape: BoxShape.circle,
backgroundColor: Colors.blue[500],
),
child: new Center(
child: new Icon(
Icons.check,
color: Colors.white,
),
)
);
Any insight on what's going on?
AnimatedContainer supports animting it's transform value, as follow:
/// scale to 95%, centerred
final width = 200.0;
final height = 300.0;
bool shouldScaleDown = true;// change value when needed
AnimatedContainer(
color: Colors.blueAccent,
width: width,
height: height,
duration: Duration(milliseconds: 100),
transform: (shouldScaleDown
? (Matrix4.identity()
..translate(0.025 * width, 0.025 * height)// translate towards right and down
..scale(0.95, 0.95))// scale with to 95% anchorred at topleft of the AnimatedContainer
: Matrix4.identity()),
child: Container(),
);
I'm afraid transform is one of the properties we don't animate (child is another). If you want to animate the scale, you can use ScaleTransition.
ScaleTransition: https://docs.flutter.io/flutter/widgets/ScaleTransition-class.html
Bug for Matrix lerp: https://github.com/flutter/flutter/issues/443
you can Animate using the Animated Builder,the below code will scale a Text from font Size 20-35 in 4 secs let me break this into steps to make you better understand
1.you need to implement your class from TickerProviderStateMixin.
2.You need an AnimationController and a Animation variables;
3.wrap your widget inside an AnimatedBuilder (note AnimatedBuilder must return a Widget at least a container) and add a controller to the animation as
animation: _controller,
and builder Which returns the AnimatedWidget
4.In the init method intialize the controller with vsync and the Duration of Animation.
and the animation with the Tweenit takes begin and end values which define the initial and final values of your Widget to be animated
for me, in this case, it was text widget so the begin and end Values will be corresponding to the fontSize.which receives variable values as animation.value
5.Finally How do you want the animation effect To be will be specified by the animate which takes in controller and the Curve effect
Here is a Working example
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<SplashScreen>
with TickerProviderStateMixin {
AnimationController _controller;
Animation _animation;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFF005CA9),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Container(
child: Text(
'Hello World',
style: TextStyle(
color: Colors.white,
fontSize: _animation.value,
),
),
);
},
),
));
}
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 4));
_animation = Tween<double>(
begin: 20,
end: 35,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.ease,
),
);
_controller.forward();
}
}
the code above produces the following output
I'm having a hard time understanding how to best create a scrollable container for the body that holds inside children that by default are scrollable as well.
In this case the grid shouldn't scroll but it's the entire page that should scroll so you are able to see more of the elements inside the grid. So basically the whole content should move vertically with the addition of the ListView moving horizontally (but that works fine already)
I had it working but it was using a bunch of "silver" widget, and I'm hoping there's a better solution that works without using all those extra widgets.
Thanks
Here's my code so far:
class GenresAndMoodsPage extends AbstractPage {
#override
String getTitle() => 'Genres & Moods';
#override
int getPageBottomBarIndex() => BottomBarItems.Browse.index;
static const kListHeight = 150.0;
Widget _buildHorizontalList() => SizedBox(
height: kListHeight,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 20,
itemBuilder: (_, index) =>
CTile(heading: 'Hip Hop', subheading: '623 Beats'),
),
);
Widget _buildGrid() => GridView.count(
crossAxisCount: 2,
crossAxisSpacing: LayoutSpacing.sm,
mainAxisSpacing: LayoutSpacing.sm,
children: List.generate(10, (index) {
return CTile(
padding: false,
heading: 'Kevin Gates Type Beat',
subheading: '623 FOLLOWERS',
width: double.infinity,
);
}),
);
#override
Widget buildBody(_) {
return ListView(children: [
CSectionHeading('Popular Genres & Moods'),
_buildHorizontalList(),
CSectionHeading('All Genres & Moods'),
_buildGrid(),
]);
}
}
The result should be something like this
Create List with Horizontal Scroll direction and called it as a child for Vertical Scroll direction.
body: new ListView.builder(itemBuilder: (context, index){
return new HorizList();
})
class HorizList extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new Container(
height: 100.0,
child: new ListView.builder(itemBuilder: (context, index){
return new Card(child: new Container(width: 80.0,
child: new Text('Hello'),alignment: Alignment.center,));
}, scrollDirection: Axis.horizontal,),
);
}
}
As we want Popular Genres & Moods section also to scroll, we should not using nestedScroll. In above example GridView is nested inside `ListView. Because of which when we scroll, only the GridView will scroll.
I used Only one ListView to achieve the similar screen.
Number of children = (AllGenresAndMoodsCount/2) + 1
divide by 2 as we are having 2 elements per row
+1 for the first element which is horizontal scroll view.
Please refer the code:
import 'package:flutter/material.dart';
void main() {
runApp(new Home());
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
var image = new Image.network("http://www.gstatic.com/webp/gallery/1.jpg");
var container = new Container(
child: image,
padding: EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0, bottom: 5.0),
width: 200.0,
height: 200.0,
);
return MaterialApp(
title: "Scroller",
home: Scaffold(
body: Center(
child: new ListView.builder(
itemBuilder: (context, index) {
if (index == 0) { //first row is horizontal scroll
var singleChildScrollView = SingleChildScrollView(
child: Row(
children: <Widget>[
container,
container,
container,
],
),
scrollDirection: Axis.horizontal);
return singleChildScrollView;
} else {
return new Row(
children: <Widget>[container, container],
);
}
},
itemCount: 10, // 9 rows of AllGenresAndMoods + 1 row of PopularGenresAndMoods
)),
),
);
}
}