Reputation: 6637
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(...)
]);
Upvotes: 213
Views: 263163
Reputation: 991
This approach:
itemCount
.Flutter estimates the ScrollPosition.maxScrollExtent
based on visible elements, and ListView.builder
only renders visible elements (lazy loading).
Adding ScrollPosition.viewportDimension
and ScrollPosition.maxScrollExtent
yields the full content height, which is used to estimate the position of the element at a given 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,
),
)
],
),
);
}
}
Upvotes: 30
Reputation: 425
you can refer following code to the scroll-specific position in the listview builder that is perfectly working for me.
ListView.builder(
controller: controller,
itemCount: 15,
itemBuilder: (BuildContext context, int index) {
return children[index);
},
),
ElevatedButton(
onPressed: () {
oneScrollController .animateTo(10 * 10,
duration: const Duration(milliseconds: 10),
curve: Curves.bounceIn);
},
child: Text(
"Contact me",);
);
Upvotes: -1
Reputation: 13603
You perfectly can use ensureVisibility()
but need to convert your ListView
into a SingleChildScrollView
so that you are not affected by the ListView
own widget visibility optimization process (not all the widget are actually rendered at a given time).
Here is an example that I'm sure will benefit many of you: An interactive debugger that let you follow a program being ran in live.
Say you have a list of Widgets
that materializes the program instructions to be executed and you want to follow the execution by looking at the running instruction no matter where in the program it is located.
Define a unique GlobalKey that designates you running instruction.
In the build, you set the key to the currently running instruction (aka the widget you always want to be visible) while let null to the others.
Then simply add a WidgetsBinding.instance.addPostFrameCallback
within which you call the Scrollable.ensureVisible
passing the running instruction widget context.
And voilà.
Here is a working DartPad
class Debugger extends StatelessWidget {
const Debugger(Simulator simulator);
/// The key that designates the visible widget
final currentIpKey = GlobalKey();
List<Widget> get steps {
final l = <Widgets>[];
for(int i=0; i<simulator.program.length; i++) {
// == Assign the key to the widget we wanna see, while
// leaving null to others
final key = i == simulator.ip ? currentIpKey : null;
l.add(Text(key:key, simulator.program[i]);
}
return l;
}
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Battery BMS simulator"),
body: Card(
child: AnimatedBuilder(
animation: widget.simulator,
builder: (context, child) {
final allSteps = steps;
// == Add the post build call to scroll to the visible
// widget.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (currentIpKey.currentContext != null)
Scrollable.ensureVisible(
currentIpKey.currentContext!,
duration: Duration(seconds: 1),
);
});
// New code:
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(children: allSteps),
);
// Former code
//return ListView(
// physics: const BouncingScrollPhysics(),
// children: allSteps,
//);
}),
),
);
}
}
Upvotes: 3
Reputation: 47
My Solution
class SitePage extends StatelessWidget {
const SitePage({super.key});
@override Widget build(BuildContext context) {
GlobalKey<State<StatefulWidget>> dataKey = new GlobalKey();
return Scaffold(
backgroundColor: SiteColors.backgroundColor,
body: SingleChildScrollView(
child: Column(
children: [
HeaderNavigation(dataKey: dataKey),
HeaderModule(),
CategoriesWidget(dataKey: dataKey),
58.verticalSpaceFromWidth,
],
),
),
);
}
}
Upvotes: -1
Reputation: 11
Studying some of the other approaches I came up to a solution with the use of ListView and trying to get the height of each list item through its context.
First, I wrap each list item with this widget:
class _ListItem extends StatelessWidget {
const _ListItem(
{Key? key,
required this.onItemBuilt,
required this.index,
required this.child})
: super(key: key);
final int index;
final Widget child;
final void Function(double) onItemBuilt;
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (context.mounted && context.size != null) {
onItemBuilt(context.size!.height);
}
});
return child;
}
}
addPostFrameCallback is used to obtain item's height after the item is built. Then the item's height is exposed through the onItemBuilt callback.
Second, I get each item's height when ListView builds the items.
ListView(
children: items.map((e) {
final index = widget.children.indexOf(e);
return _ListItem(
index: index,
child: e,
onItemBuilt: (height) => _setItemHeights(index, height));
}).toList(),
)
Then I need to store the heights of the items through the method _setItemHeights:
final _itemHeights = <double>[];
void _setItemHeights(int index, double height) {
// Assure that item's height is not already in _itemHeights list
if (index >= _itemHeights.length) {
_itemHeights.add(height);
}
}
Third, to scroll to a specific item we need to calculate the offset. Since we have the heights of the items stored to _itemHeights, we call the method:
double _getOffset(int index) {
double offset = 0;
for (int i = 0; i < index; i++) {
if (i < _itemHeights.length) {
offset = offset + _itemHeights[i];
}
}
return offset;
}
and then call:
_scrollController.animateTo(_getOffset(index),
duration: const Duration(milliseconds: 160),
curve: Curves.linear)
BUT, ListView does not build all the items from the beggining (ListView builts the items that are visible from Viewport and some of the next items and when user scrolls, then ListView builds the following items that are going to be visible from Viewport).
So, when we want to scroll to an item that has not been built yet, then we should wait for the ListView to build all the preceding items. That is achieved by scrolling to the last item of our _itemHeights list in order to trigger ListView to build more items until the desired item is built. To implement that we create the method:
int _tempItemHeightsLength = 0;
Future<void> _animateToItem(int index) async {
final itemHeightsLength = _itemHeights.length;
if (_scrollController.hasClients) {
if (index <= itemHeightsLength) {
await _scrollController.animateTo(
_getOffset(index),
duration: const Duration(milliseconds: 160),
curve: Curves.linear);
} else if (_tempItemHeightsLength < itemHeightsLength) {
_tempItemHeightsLength = itemHeightsLength;
await _scrollController.animateTo(
_getOffset(itemHeightsLength - 1),
duration: const Duration(milliseconds: 160),
curve: Curves.linear);
await _animateToItem(index);
}
}
}
Finally, I created a custom controller (it is shown below) to manipulate scrolling with an extra property extraPixels in order to position the item some pixels lower or higher regarding Viewport's top position.
That's it. For me this solution works very well. I also added a property padding for ListView.
The entire code is below:
import 'package:flutter/material.dart';
/// A wrapper around ListView with a controller (CustomListViewController)
/// which features method animateToItem(int index) that scrolls the ListView
/// to the item through its index
class CustomListView extends StatefulWidget {
const CustomListView(
{Key? key,
required this.controller,
required this.children,
this.scrollAnimationDuration, this.padding})
: super(key: key);
final CustomListViewController controller;
final List<Widget> children;
final Duration? scrollAnimationDuration;
final EdgeInsets? padding;
@override
State<CustomListView> createState() => _CustomListViewState();
}
class _CustomListViewState extends State<CustomListView> {
late final ScrollController _scrollController;
final _itemHeights = <double>[];
int _tempItemHeightsLength = 0;
Future<void> _initialize(int index, double? extraPixels) async {
assert(index >= 0 && index < widget.children.length,
'Index of item to animate to is out of list\'s range');
if (_itemHeights.isNotEmpty) {
await _animateToItem(index, extraPixels);
} else {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (_itemHeights.isNotEmpty) {
_animateToItem(index, extraPixels);
}
});
}
}
Future<void> _animateToItem(int index, double? extraPixels) async {
final itemHeightsLength = _itemHeights.length;
if (_scrollController.hasClients) {
if (index <= itemHeightsLength) {
await _scrollController.animateTo(
_getOffset(index) - (extraPixels ??= 0),
duration: widget.scrollAnimationDuration ??
const Duration(milliseconds: 160),
curve: Curves.linear);
} else if (_tempItemHeightsLength < itemHeightsLength &&
itemHeightsLength > 0) {
_tempItemHeightsLength = itemHeightsLength;
await _scrollController.animateTo(_getOffset(itemHeightsLength - 1),
duration: widget.scrollAnimationDuration ??
const Duration(milliseconds: 160),
curve: Curves.linear);
await _animateToItem(index, extraPixels);
}
}
}
void _setItemHeights(int index, double height) {
if (index >= _itemHeights.length) {
_itemHeights.add(height);
}
}
double _getOffset(int index) {
double offset = 0;
for (int i = 0; i < index; i++) {
if (i < _itemHeights.length) {
offset = offset + _itemHeights[i];
}
}
return offset + (widget.padding?.top?? 0);
}
@override
void initState() {
_scrollController = widget.controller.scrollController;
widget.controller._state = this;
super.initState();
}
@override
Widget build(BuildContext context) {
return ListView(
controller: _scrollController,
padding: widget.padding,
children: widget.children.map((e) {
final index = widget.children.indexOf(e);
return _ListItem(
index: index,
child: e,
onItemBuilt: (height) => _setItemHeights(index, height));
}).toList(),
);
}
}
/// Wrapper around the items of ListView.
/// The CallBack onItemBuilt exposes the height of the item to the CutomListView,
/// so that the offset of the scroll position can be calculated later on.
class _ListItem extends StatelessWidget {
const _ListItem(
{Key? key,
required this.onItemBuilt,
required this.index,
required this.child})
: super(key: key);
final int index;
final Widget child;
final void Function(double) onItemBuilt;
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (context.mounted && context.size != null) {
onItemBuilt(context.size!.height);
}
});
return child;
}
}
/// Controller of CustomListView.
/// It includes a ScrollController that is attached to the ListView and
/// can be used at will (e.g. to add a ScrollBar)
/// and the method animateToItem to scroll the ListView to a specific item.
class CustomListViewController {
_CustomListViewState? _state;
/// ScrollController that is attached to ListView
final scrollController = ScrollController();
/// Method to scroll ListView to specific item given the item's index.
/// The item appears first in ListView's Viewport.
/// With extraPixels, pixels can be added/subtracted in order to position the
/// item lower or higher in Viewport.
/// If ListView is built before calling this method, the Future of this
/// method is returned when ListView completes the scrolling to specific item.
/// Otherwise this method is scheduled for next Frame,
/// therefore the Future is returned earlier and it is not bound to the
/// completion of the scrolling
Future<void> animateToItem(int index, {double? extraPixels}) async {
if (_state != null) {
await _state!._initialize(index, extraPixels);
} else {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
_state?._initialize(index, extraPixels);
});
}
}
}
I hope this solution is helpful :)
Upvotes: 0
Reputation: 253
Well this same answer https://stackoverflow.com/a/49154882/8422048
But I came up with a solution to fix this problem. this is a trick. If you want to scroll to unbuilt widget. You can use 2 or 3 times Scrollable.ensureVisible
Example:
final _datakey = GlobalKey();
final _datakey2 = GlobalKey();
============================
await Scrollable.ensureVisible(dataKey.currentContext!,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 250)),
await Future.delayed(const Duration(milliseconds: 200)),
await Scrollable.ensureVisible(dataKey2.currentContext!,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 250)),
===============
SingleChildScrollView(
child: Column(
children: [
Container(
key: dataKey,
width: 500,
height: 500,
color: Colors.yellow,
),
Container(
width: 500,
height: 500,
color: Colors.red,
),
Container( // this is a widget, which u want to scroll
key: dataKey2,
width: 500,
height: 500,
color: Colors.blue,
),
],
),
);
Upvotes: 0
Reputation: 770
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.
Upvotes: 2
Reputation: 19494
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);
Upvotes: 50
Reputation: 2235
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.
Upvotes: 0
Reputation: 268114
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')),
),
);
},
),
);
}
}
Upvotes: 58
Reputation: 6654
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:
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
Upvotes: 97
Reputation: 61
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),
);
}
}
Upvotes: 6
Reputation: 34250
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);
}
}
Upvotes: 5
Reputation: 41
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),
),
],
),
);
}
}
Upvotes: 2
Reputation: 15
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",),
Upvotes: -2
Reputation: 41
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);
}
Upvotes: 3
Reputation: 2838
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
Upvotes: 31
Reputation: 2000
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;
}
}
Upvotes: 7
Reputation: 618
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));
});
Upvotes: 0
Reputation: 277447
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.
Upvotes: 277
Reputation: 34809
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"),
),
],
),
);
}
}
Upvotes: 11