I want to add my ListView.seperated into Column, so that I can add CircularProgressIndicator below this ListvView when loading more items. I have used advices from How to add a ListView to a Column in Flutter? so that I made structure Column -> Expanded -> ListVie, but I got errors and list cannot be loaded:
======== Exception caught by rendering library =====================================================
RenderBox was not laid out: RenderRepaintBoundary#e8fad NEEDS-LAYOUT NEEDS-PAINT
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1929 pos 12: 'hasSize'
The relevant error-causing widget was:
The relevant error-causing widget was:
Column file:///C://lib/ui/pages/home/page/HomePage.dart:125:16
When the exception was thrown, this was the stack:
#0 RenderFlex.performLayout.<anonymous closure> (package:flutter/src/rendering/flex.dart:926:9)
#1 RenderFlex.performLayout (package:flutter/src/rendering/flex.dart:929:6)
#2 RenderObject.layout (package:flutter/src/rendering/object.dart:1781:7)
#3 ChildLayoutHelper.layoutChild (package:flutter/src/rendering/layout_helper.dart:54:11)
#4 RenderFlex._computeSizes (package:flutter/src/rendering/flex.dart:829:43)
Column file:///C:/.../.../lib/ui/pages/home/page/HomePage.dart:125:16
Could you tell me what have I done wrong? This AdvertisementTile builds new widgets, but it is built onto another column. Maybe this is wrong?
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:.../ui/pages/home/page/AdvertisementCard.dart';
import 'package:.../ui/pages/home/page/model/AdvertisementList.dart';
import '../../SizedBox.dart';
import 'AdvertisementProdRepository.dart';
import 'BottomAppBar.dart';
import 'FAB.dart';
import 'model/AdvertisementList.dart';
class HomePage extends StatefulWidget {
final String jwt;
const HomePage(this.jwt);
#override
_HomePage createState() => _HomePage();
factory HomePage.fromBase64(String jwt) => HomePage(jwt);
}
class _HomePage extends State<HomePage> {
late final String jwt;
late Future<AdvertisementList> _listOfItems;
final searchTextController = TextEditingController();
#override
void initState() {
super.initState();
jwt = widget.jwt;
_listOfItems = AdvertisementProdRepository.fetchAdvertisements(1);
}
#override
Widget build(BuildContext context) => Scaffold(
body: Scaffold(
backgroundColor: const Color(0xFEF9F9FC),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerDocked,
floatingActionButton: buildFAB(),
bottomNavigationBar: BuildBottomAppBar(),
body: Container(
padding: EdgeInsets.only(left: 25.0, right: 25, top: 25),
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
children: [
TextFormField(
controller: searchTextController,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
hintText: 'Szukaj',
fillColor: Color(0xffeeeeee),
filled: true),
),
buildSizedBox(20.0),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
'Najnowsze ogłoszenia',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
textAlign: TextAlign.left,
),
],
),
),
buildSizedBox(10.0),
FutureBuilder<AdvertisementList>(
future: _listOfItems,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return AdvertisementTile(advertisements: snapshot.data!);
}
},
),
],
),
),
),
);
}
class AdvertisementTile extends StatefulWidget {
final AdvertisementList advertisements;
AdvertisementTile({Key? key, required this.advertisements}) : super(key: key);
#override
State<StatefulWidget> createState() => AdvertisementTileState();
}
class AdvertisementTileState extends State<AdvertisementTile> {
AdvertisementLoadMoreStatus loadMoreStatus =
AdvertisementLoadMoreStatus.STABLE;
final ScrollController scrollController = new ScrollController();
late List<Advertisement> advertisements;
late int currentPageNumber;
bool _loading = false;
#override
void initState() {
advertisements = widget.advertisements.items;
currentPageNumber = widget.advertisements.pageNumber;
super.initState();
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: onNotification,
child: Column(
children: <Widget>[
Expanded(
child: ListView.separated(
padding: EdgeInsets.zero,
scrollDirection: Axis.vertical,
controller: scrollController,
itemCount: advertisements.length,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (_, index) {
return AdvertisementCard(data: advertisements[index]);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 10,
);
},
),
),
],
),
);
}
/*child: Padding(
padding: const EdgeInsets.only(bottom: 28.0),*/
bool onNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
if (scrollController.position.maxScrollExtent > scrollController.offset &&
scrollController.position.maxScrollExtent - scrollController.offset <=
50) {
if (loadMoreStatus == AdvertisementLoadMoreStatus.STABLE) {
loadMoreStatus = AdvertisementLoadMoreStatus.LOADING;
_loading = true;
AdvertisementProdRepository.fetchAdvertisements(currentPageNumber + 1)
.then((advertisementObject) {
currentPageNumber = advertisementObject.pageNumber;
loadMoreStatus = AdvertisementLoadMoreStatus.STABLE;
setState(() => advertisements.addAll(advertisementObject.items));
_loading = false;
});
}
}
}
return true;
}
}
enum AdvertisementLoadMoreStatus { LOADING, STABLE }
So the way a Column behaves is that when you give it 2 children, the flutter engine estimates the space that the single widget would cover, and then it would render those two widgets accordingly.
An Expanded widget tells flutter to take a widget and then cover as much space as that widget can take. In some cases, it makes sense to use an Expanded widget if the other widget is wrapped in a Container, this way flutter knows that you are referring to the remaining space.
ListView is a widget that takes all available spaces available.
So to fix your render issue you can put the Listview in a Container and give the Container a fixed height then flutter knows how to manage your screen and avoid the rendering issue.
If you eventually get any, you'll still have most of your UI, then you'll just have to tweak the height of your Container.
E.g
Container(
height: 400,
child: ListView.separated(
itemBuilder: (context, index) {
return AdvertisementCard(
data: advertisements[index]);
},
separatorBuilder: (context, index) {
return SizedBox(
height: 10,
);
},
itemCount: reports.length),
)
You should constrain the height of the Listview as the Column wants to get all the available height of its parent (likely Scaffold in your case). So, an unbounded-height-widget as a Listview will cause this kind of error.
Solution: wrap the Listview with a Container of some height.
I'm trying to achieve this functionality on flutter but honestly have no idea how to do it. I've been trying to figure it out for weeks, I tried flutter_staggered_grid_view, which was kind of the closest to this, but that didn't help either. Does anyone have any idea on how to achieve this effect?
You can use the Wrap widget as grid, and use some custom widget with AnimatedContainer to expand and retract the bloc.
//number of childs used in the example
static const itemCount = 8;
//list of each bloc expandable state, that is changed to trigger the animation of the AnimatedContainer
List<bool> expandableState = List.generate(itemCount, (index) => false);
Widget bloc (double width, int index) {
bool isExpanded = expandableState[index];
return GestureDetector(
onTap: () {
setState(() {
//changing the current expandableState
expandableState[index] = !isExpanded;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
margin: const EdgeInsets.all(20.0),
width: !isExpanded ? width * 0.4 : width * 0.8,
height: !isExpanded ? width * 0.4 : width * 0.8,
color: Colors.red,
),
);
}
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return Scaffold(
body: Align(
child: SingleChildScrollView(
child: Wrap(
children: List.generate(itemCount, (index) {
return bloc(width, index);
}),
),
),
),
);
}
You could have a list of the items that should be expanded and lay them out in the grid view accordingly (using flutter_staggered_grid_view for example).
I edited the example written in the library docs to achieve the following result:
Basically,
create a StatefulWidget and add a list to it (_expandedIndices). The purpose of the list is to keep track of the indices of the items that are expanded.
add a GestureDetector on the grid cells to detect taps and add/remove indices from the list (add the index to the list if it is not already there, otherwise remove it).
Don't forget to put the code that updates the list inside a setState.
in the staggeredTileBuilder configure crossAxisCellCount and mainAxisCellCount based on whether the item in index should be expanded or not.
class StaggeredGridViewWithExpandableCells extends StatefulWidget {
#override
_StaggeredGridViewWithExpandableCellsState createState() =>
_StaggeredGridViewWithExpandableCellsState();
}
class _StaggeredGridViewWithExpandableCellsState
extends State<StaggeredGridViewWithExpandableCells> {
final _expandedIndices = Set<int>();
#override
Widget build(BuildContext context) {
return StaggeredGridView.countBuilder(
crossAxisCount: 4,
itemCount: 16,
itemBuilder: (BuildContext context, int index) => GestureDetector(
onTap: () => setState(() => _expandedIndices.contains(index) ? _expandedIndices.remove(index) : _expandedIndices.add(index)),
child: new Container(
color: Colors.green,
child: new Center(
child: new CircleAvatar(
backgroundColor: Colors.white,
child: new Text('$index'),
),
)),
),
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(_expandedIndices.contains(index) ? 4 : 2, 1),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
);
}
}
You can use Wrap widget to achieve the result. Please see the code below.
import 'package:flutter/material.dart';
final Color darkBlue = const Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: const Text("Demo")),
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final int _cells = 8;
final double _containerSizeSmall = 75;
final double _containerSizeLarge = 170;
final double _padding = 10;
int _clicked = 0;
#override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
return SingleChildScrollView(
child: Container(
height: size.height,
width: 240,
child: Wrap(
children: List.generate(
_cells,
(col) => Padding(
padding: EdgeInsets.all(_padding),
child: GestureDetector(
onTap: () {
setState(() {
_clicked != col + 1 ? _clicked = col + 1 : _clicked = 0;
});
},
child: Container(
height: _clicked == col + 1
? _containerSizeLarge
: _containerSizeSmall,
width: _clicked == col + 1
? _containerSizeLarge
: _containerSizeSmall,
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: const BorderRadius.all(
const Radius.circular(5),
),
),
child: Center(child: Text('${col + 1}')),
),
),
),
),
),
),
);
}
}
I am trying to create a scrollView using CustomScrollView.
The effect that I need, is very similar to this one.
I need the SliverList to be stacked above the SliverAppbar, without the list taking the whole screen and hiding the SliverAppbar.
The reason I want to do this, is that i need to attach a persistent Positioned widget on top of that list, and it won't appear unless the list is stacked above the SliverAppbar.
Here's my code.
Step one:
Use ListView inside SliverAppBar widget. To make css overflow:hidden effect.
Step two:
Add controller to NestedScrollView and move the button on scrolling in a stack. Plus calculate where you want to stop button moving.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController scrollController;
final double expandedHight = 150.0;
#override
void initState() {
super.initState();
scrollController = new ScrollController();
scrollController.addListener(() => setState(() {}));
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
double get top {
double res = expandedHight;
if (scrollController.hasClients) {
double offset = scrollController.offset;
if (offset < (res - kToolbarHeight)) {
res -= offset;
} else {
res = kToolbarHeight;
}
}
return res;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
NestedScrollView(
controller: scrollController,
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
pinned: true,
expandedHeight: expandedHight,
flexibleSpace: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [
AppBar(
title: Text('AfroJack'),
elevation: 0.0,
),
Container(
color: Colors.blue,
height: 100,
alignment: Alignment.center,
child: RaisedButton(
child: Text('folow'),
onPressed: () => print('folow pressed'),
),
),
],
),
),
];
},
body: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 80,
itemBuilder: (BuildContext context, int index) {
return Text(
'text_string'.toUpperCase(),
style: TextStyle(
color: Colors.white,
),
);
},
),
),
Positioned(
top: top,
width: MediaQuery.of(context).size.width,
child: Align(
child: RaisedButton(
onPressed: () => print('shuffle pressed'),
child: Text('Suffle'),
),
),
),
],
),
);
}
}
How can I scroll to a special widget in a ListView?
For instance I want to scroll automatically to some Container in the ListView if I press a specific button.
ListView(children: <Widget>[
Container(...),
Container(...), #scroll for example to this container
Container(...)
]);
By far, the easiest solution is to use Scrollable.ensureVisible(context). As it does everything for you and work with any widget size. Fetching the context using GlobalKey.
The problem is that ListView won't render non-visible items. Meaning that your target most likely will not be built at all. Which means your target will have no context ; preventing you from using that method without some more work.
In the end, the easiest solution will be to replace your ListView by a SingleChildScrollView and wrap your children into a Column. Example :
class ScrollView extends StatelessWidget {
final dataKey = new GlobalKey();
#override
Widget build(BuildContext context) {
return new Scaffold(
primary: true,
appBar: new AppBar(
title: const Text('Home'),
),
body: new SingleChildScrollView(
child: new Column(
children: <Widget>[
new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
// destination
new Card(
key: dataKey,
child: new Text("data\n\n\n\n\n\ndata"),
)
],
),
),
bottomNavigationBar: new RaisedButton(
onPressed: () => Scrollable.ensureVisible(dataKey.currentContext),
child: new Text("Scroll to data"),
),
);
}
}
NOTE : While this allows to scroll to the desired item easily, consider this method only for small predefined lists. As for bigger lists you'll get performance problems.
But it's possible to make Scrollable.ensureVisible work with ListView ; although it will require more work.
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 ListView scroll to index not available
(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:
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));
Please not that although the scrollable_positioned_list package is published by google.dev, they explicitly state that their packages are not officially supported Google products. - Source
Screenshot (Fixed height content)
If your items have fixed height, then you can use the following approach.
class HomePage extends StatelessWidget {
final ScrollController _controller = ScrollController();
final double _height = 100.0;
void _animateToIndex(int index) {
_controller.animateTo(
index * _height,
duration: Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.arrow_downward),
onPressed: () => _animateToIndex(10),
),
body: ListView.builder(
controller: _controller,
itemCount: 20,
itemBuilder: (_, i) {
return SizedBox(
height: _height,
child: Card(
color: i == 10 ? Colors.blue : null,
child: Center(child: Text('Item $i')),
),
);
},
),
);
}
}
For people are trying to jump to widget in CustomScrollView.
First, add this plugin to your project.
Then look at my example code below:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
AutoScrollController _autoScrollController;
final scrollDirection = Axis.vertical;
bool isExpaned = true;
bool get _isAppBarExpanded {
return _autoScrollController.hasClients &&
_autoScrollController.offset > (160 - kToolbarHeight);
}
#override
void initState() {
_autoScrollController = AutoScrollController(
viewportBoundaryGetter: () =>
Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
axis: scrollDirection,
)..addListener(
() => _isAppBarExpanded
? isExpaned != false
? setState(
() {
isExpaned = false;
print('setState is called');
},
)
: {}
: isExpaned != true
? setState(() {
print('setState is called');
isExpaned = true;
})
: {},
);
super.initState();
}
Future _scrollToIndex(int index) async {
await _autoScrollController.scrollToIndex(index,
preferPosition: AutoScrollPosition.begin);
_autoScrollController.highlight(index);
}
Widget _wrapScrollTag({int index, Widget child}) {
return AutoScrollTag(
key: ValueKey(index),
controller: _autoScrollController,
index: index,
child: child,
highlightColor: Colors.black.withOpacity(0.1),
);
}
_buildSliverAppbar() {
return SliverAppBar(
brightness: Brightness.light,
pinned: true,
expandedHeight: 200.0,
backgroundColor: Colors.white,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: BackgroundSliverAppBar(),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(40),
child: AnimatedOpacity(
duration: Duration(milliseconds: 500),
opacity: isExpaned ? 0.0 : 1,
child: DefaultTabController(
length: 3,
child: TabBar(
onTap: (index) async {
_scrollToIndex(index);
},
tabs: List.generate(
3,
(i) {
return Tab(
text: 'Detail Business',
);
},
),
),
),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _autoScrollController,
slivers: <Widget>[
_buildSliverAppbar(),
SliverList(
delegate: SliverChildListDelegate([
_wrapScrollTag(
index: 0,
child: Container(
height: 300,
color: Colors.red,
)),
_wrapScrollTag(
index: 1,
child: Container(
height: 300,
color: Colors.red,
)),
_wrapScrollTag(
index: 2,
child: Container(
height: 300,
color: Colors.red,
)),
])),
],
),
);
}
}
Yeah it's just a example, use your brain to make it this idea become true
This solution improves upon other answers as it does not require hard-coding each elements' heights. Adding ScrollPosition.viewportDimension and ScrollPosition.maxScrollExtent yields the full content height. This can be used to estimate the position of an element at some index. If all elements are the same height, the estimation is perfect.
// Get the full content height.
final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
// Index to scroll to.
final index = 100;
// Estimate the target scroll position.
final target = contentSize * index / itemCount;
// Scroll to that position.
controller.position.animateTo(
target,
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
);
And a full example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter Test",
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final controller = ScrollController();
final itemCount = 1000;
return Scaffold(
appBar: AppBar(
title: Text("Flutter Test"),
),
body: Column(
children: [
ElevatedButton(
child: Text("Scroll to 100th element"),
onPressed: () {
final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
final index = 100;
final target = contentSize * index / itemCount;
controller.position.animateTo(
target,
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
);
},
),
Expanded(
child: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return ListTile(
title: Text("Item at index $index."),
);
},
itemCount: itemCount,
),
)
],
),
);
}
}
You can use GlobalKey to access buildercontext.
I use GlobalObjectKey with Scrollable.
Define GlobalObjectKey in item of ListView
ListView.builder(
itemCount: category.length,
itemBuilder: (_, int index) {
return Container(
key: GlobalObjectKey(category[index].id),
You can navigate to item from anywhere
InkWell(
onTap: () {
Scrollable.ensureVisible(GlobalObjectKey(category?.id).currentContext);
You add scrollable animation changing property of ensureVisible
Scrollable.ensureVisible(
GlobalObjectKey(category?.id).currentContext,
duration: Duration(seconds: 1),// duration for scrolling time
alignment: .5, // 0 mean, scroll to the top, 0.5 mean, half
curve: Curves.easeInOutCubic);
You can just specify a ScrollController to your listview and call the animateTo method on button click.
A mininmal example to demonstrate animateTo usage :
class Example extends StatefulWidget {
#override
_ExampleState createState() => new _ExampleState();
}
class _ExampleState extends State<Example> {
ScrollController _controller = new ScrollController();
void _goToElement(int index){
_controller.animateTo((100.0 * index), // 100 is the height of container and index of 6th element is 5
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: new Column(
children: <Widget>[
new Expanded(
child: new ListView(
controller: _controller,
children: Colors.primaries.map((Color c) {
return new Container(
alignment: Alignment.center,
height: 100.0,
color: c,
child: new Text((Colors.primaries.indexOf(c)+1).toString()),
);
}).toList(),
),
),
new FlatButton(
// on press animate to 6 th element
onPressed: () => _goToElement(6),
child: new Text("Scroll to 6th element"),
),
],
),
);
}
}
Here is the solution for StatefulWidget if you want to made widget visible right after building the view tree.
By extending Remi's answer, you can achieve it with this code:
class ScrollView extends StatefulWidget {
// widget init
}
class _ScrollViewState extends State<ScrollView> {
final dataKey = new GlobalKey();
// + init state called
#override
Widget build(BuildContext context) {
return Scaffold(
primary: true,
appBar: AppBar(
title: const Text('Home'),
),
body: _renderBody(),
);
}
Widget _renderBody() {
var widget = SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: 1160.0, width: double.infinity, child: new Card()),
SizedBox(height: 420.0, width: double.infinity, child: new Card()),
SizedBox(height: 760.0, width: double.infinity, child: new Card()),
// destination
Card(
key: dataKey,
child: Text("data\n\n\n\n\n\ndata"),
)
],
),
);
setState(() {
WidgetsBinding.instance!.addPostFrameCallback(
(_) => Scrollable.ensureVisible(dataKey.currentContext!));
});
return widget;
}
}
Output:
Use Dependency:
dependencies:
scroll_to_index: ^1.0.6
Code: (Scroll will always perform 6th index widget as its added below as hardcoded, try with scroll index which you required for scrolling to specific widget)
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);
}
}
I found a perfect solution to it using ListView.
I forgot where the solution comes from, so I posted my code. This credit belongs to other one.
21/09/22:edit. I posted a complete example here, hope it is clearer.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class CScrollToPositionPage extends StatefulWidget {
CScrollToPositionPage();
#override
State<StatefulWidget> createState() => CScrollToPositionPageState();
}
class CScrollToPositionPageState extends State<CScrollToPositionPage> {
static double TEXT_ITEM_HEIGHT = 80;
final _formKey = GlobalKey<FormState>();
late List _controls;
List<FocusNode> _lstFocusNodes = [];
final __item_count = 30;
#override
void initState() {
super.initState();
_controls = [];
for (int i = 0; i < __item_count; ++i) {
_controls.add(TextEditingController(text: 'hello $i'));
FocusNode fn = FocusNode();
_lstFocusNodes.add(fn);
fn.addListener(() {
if (fn.hasFocus) {
_ensureVisible(i, fn);
}
});
}
}
#override
void dispose() {
super.dispose();
for (int i = 0; i < __item_count; ++i) {
(_controls[i] as TextEditingController).dispose();
}
}
#override
Widget build(BuildContext context) {
List<Widget> widgets = [];
for (int i = 0; i < __item_count; ++i) {
widgets.add(TextFormField(focusNode: _lstFocusNodes[i],controller: _controls[i],));
}
return Scaffold( body: Container( margin: const EdgeInsets.all(8),
height: TEXT_ITEM_HEIGHT * __item_count,
child: Form(key: _formKey, child: ListView( children: widgets)))
);
}
Future<void> _keyboardToggled() async {
if (mounted){
EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets;
while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) {
await Future.delayed(const Duration(milliseconds: 10));
}
}
return;
}
Future<void> _ensureVisible(int index,FocusNode focusNode) async {
if (!focusNode.hasFocus){
debugPrint("ensureVisible. has not the focus. return");
return;
}
debugPrint("ensureVisible. $index");
// Wait for the keyboard to come into view
await Future.any([Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled()]);
var renderObj = focusNode.context!.findRenderObject();
if( renderObj == null ) {
return;
}
var vp = RenderAbstractViewport.of(renderObj);
if (vp == null) {
debugPrint("ensureVisible. skip. not working in Scrollable");
return;
}
// Get the Scrollable state (in order to retrieve its offset)
ScrollableState scrollableState = Scrollable.of(focusNode.context!)!;
// Get its offset
ScrollPosition position = scrollableState.position;
double alignment;
if (position.pixels > vp.getOffsetToReveal(renderObj, 0.0).offset) {
// Move down to the top of the viewport
alignment = 0.0;
} else if (position.pixels < vp.getOffsetToReveal(renderObj, 1.0).offset){
// Move up to the bottom of the viewport
alignment = 1.0;
} else {
// No scrolling is necessary to reveal the child
debugPrint("ensureVisible. no scrolling is necessary");
return;
}
position.ensureVisible(
renderObj,
alignment: alignment,
duration: const Duration(milliseconds: 300),
);
}
}
To achieve initial scrolling at a particular index in a list of items
on tap of the floating action button you will be scrolled to an index of 10 in a list of items
class HomePage extends StatelessWidget {
final _controller = ScrollController();
final _height = 100.0;
#override
Widget build(BuildContext context) {
// to achieve initial scrolling at particular index
SchedulerBinding.instance.addPostFrameCallback((_) {
_scrollToindex(20);
});
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => _scrollToindex(10),
child: Icon(Icons.arrow_downward),
),
body: ListView.builder(
controller: _controller,
itemCount: 100,
itemBuilder: (_, i) => Container(
height: _height,
child: Card(child: Center(child: Text("Item $i"))),
),
),
);
}
// on tap, scroll to particular index
_scrollToindex(i) => _controller.animateTo(_height * i,
duration: Duration(seconds: 2), curve: Curves.fastOutSlowIn);
}
I am posting a solution here in which List View will scroll 100 pixel right and left . you can change the value according to your requirements. It might be helpful for someone who want to scroll list in both direction
import 'package:flutter/material.dart';
class HorizontalSlider extends StatelessWidget {
HorizontalSlider({Key? key}) : super(key: key);
// Dummy Month name
List<String> monthName = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"July",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
ScrollController slideController = new ScrollController();
#override
Widget build(BuildContext context) {
return Container(
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
// Here monthScroller.position.pixels represent current postion
// of scroller
slideController.animateTo(
slideController.position.pixels - 100, // move slider to left
duration: Duration(
seconds: 1,
),
curve: Curves.ease,
);
},
child: Icon(Icons.arrow_left),
),
Container(
height: 50,
width: MediaQuery.of(context).size.width * 0.7,
child: ListView(
scrollDirection: Axis.horizontal,
controller: slideController,
physics: ScrollPhysics(),
children: monthName
.map((e) => Padding(
padding: const EdgeInsets.all(12.0),
child: Text("$e"),
))
.toList(),
),
),
GestureDetector(
onTap: () {
slideController.animateTo(
slideController.position.pixels +
100, // move slider 100px to right
duration: Duration(
seconds: 1,
),
curve: Curves.ease,
);
},
child: Icon(Icons.arrow_right),
),
],
),
);
}
}
The simplest way is to call this method inside your InitState method. (not the build to evict unwanted errors)
WidgetsBinding.instance.addPostFrameCallback((_) => Scrollable.ensureVisible(targetKey.currentContext!))
WidgetsBinding.instance.addPostFrameCallback will guarantee that the list is builded and the this automatic search for your target and move the scroll to it. You can then customize the animation of the scroll effect on the Scrollable.ensureVisible method
Note: Remember to add the targetKey (a GlobalKey) to the widget you want to scroll to.
Adding with Rémi Rousselet's answer,
If there is a case you need to scroll past to end scroll position with addition of keyboard pop up, this might be hided by the keyboard. Also you might notice the scroll animation is a bit inconsistent when keyboard pops up(there is addition animation when keyboard pops up), and sometimes acts weird. In that case wait till the keyboard finishes animation(500ms for ios).
BuildContext context = key.currentContext;
Future.delayed(const Duration(milliseconds: 650), () {
Scrollable.of(context).position.ensureVisible(
context.findRenderObject(),
duration: const Duration(milliseconds: 600));
});
You can also simply use the FixedExtentScrollController for same size items with the index of your initialItem :
controller: FixedExtentScrollController(initialItem: itemIndex);
The documentation : Creates a scroll controller for scrollables whose items have the same size.
Simply use page view controller.
Example:
var controller = PageController();
ListView.builder(
controller: controller,
itemCount: 15,
itemBuilder: (BuildContext context, int index) {
return children[index);
},
),
ElevatedButton(
onPressed: () {
controller.animateToPage(5, //any index that you want to go
duration: Duration(milliseconds: 700), curve: Curves.linear);
},
child: Text(
"Contact me",),
You can use the controller.jumpTo(100) after the loading finish