[英]Flutter: Scrolling to a widget in ListView
How can I scroll to a special widget in a ListView
?如何滚动到ListView
中的特殊小部件? For instance I want to scroll automatically to some Container
in the ListView
if I press a specific button.例如,如果我按下特定按钮,我想自动滚动到ListView
中的某个Container
。
ListView(children: <Widget>[
Container(...),
Container(...), #scroll for example to this container
Container(...)
]);
By far, the easiest solution is to use Scrollable.ensureVisible(context)
.到目前为止,最简单的解决方案是使用Scrollable.ensureVisible(context)
。 As it does everything for you and work with any widget size.因为它可以为您完成所有工作并使用任何小部件大小。 Fetching the context using GlobalKey
.使用GlobalKey
获取上下文。
The problem is that ListView
won't render non-visible items.问题是ListView
不会呈现不可见的项目。 Meaning that your target most likely will not be built at all .这意味着你的目标很可能不会在所有建造。 Which means your target will have no context
;这意味着您的目标将没有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
.最后,最简单的解决方案是用SingleChildScrollView
替换您的ListView
并将您的孩子包装成一个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
;但是可以使Scrollable.ensureVisible
与ListView
; although it will require more work.虽然这将需要更多的工作。
Unfortunately, ListView has no built-in approach to a scrollToIndex() function.不幸的是,ListView 没有针对 scrollToIndex() 函数的内置方法。 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您必须开发自己的方法来测量animateTo()
或jumpTo()
元素的偏移量,或者您可以搜索这些建议的解决方案/插件或从其他帖子(如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) (自 2017 年以来,在flutter/issues/12319 上讨论了一般的 scrollToIndex 问题,但目前还没有计划)
But there is a different kind of ListView that does support scrollToIndex:但是有一种不同类型的 ListView 确实支持 scrollToIndex:
You set it up exactly like ListView and works the same, except you now have access to a ItemScrollController that does:您可以像 ListView 一样设置它并且工作方式相同,但您现在可以访问ItemScrollController ,它执行以下操作:
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.) (请注意,这个库是由 Google 开发的,而不是由 Flutter 核心团队开发的。)
If your widgets inside the ListView all have the same height, you can implement it like this:如果 ListView 中的小部件都具有相同的高度,则可以像这样实现它:
Screenshot:截屏:
For ListView
, you can try this, the following code will animate to 10th index.对于ListView
,你可以试试这个,下面的代码将动画到第 10 个索引。
class HomePage extends StatelessWidget {
final _controller = ScrollController();
final _height = 100.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => _animateToIndex(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"))),
),
),
);
}
_animateToIndex(i) => _controller.animateTo(_height * i, duration: Duration(seconds: 2), curve: Curves.fastOutSlowIn);
}
For people are trying to jump to widget in CustomScrollView .对于人们试图跳转到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是的,这只是一个例子,用你的大脑让它成为现实
You can just specify a ScrollController
to your listview and call the animateTo
method on button click.您只需为您的列表视图指定一个ScrollController
并在单击按钮时调用animateTo
方法。
A mininmal example to demonstrate animateTo
usage :演示animateTo
用法的最小示例:
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"),
),
],
),
);
}
}
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)代码:(滚动将始终执行第 6 个索引小部件,因为它在下面添加为硬编码,尝试使用滚动到特定小部件所需的滚动索引)
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
.我使用ListView
找到了一个完美的解决方案。
I forgot where the solution comes from, so I posted my code.我忘记了解决方案的来源,所以我发布了我的代码。 This credit belongs to other one.这个信用属于另一个信用。
21/09/22:edit. 21/09/22:编辑。 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),
);
}
}
Here is the solution for StatefulWidget
if you want to made widget visible right after building the view tree .如果您想在构建视图树后立即使小部件可见,这里是StatefulWidget
的解决方案。
By extending Remi's answer, you can achieve it with this code:通过扩展Remi 的答案,您可以使用以下代码实现:
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;
}
}
I am posting a solution here in which List View will scroll 100 pixel right and left .我在这里发布了一个解决方案,其中 List View 将左右滚动 100 像素。 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),
),
],
),
);
}
}
You can use GlobalKey to access buildercontext.您可以使用 GlobalKey 访问 buildercontext。
I use GlobalObjectKey
with Scrollable
.我将GlobalObjectKey
与Scrollable
一起使用。
Define GlobalObjectKey in item of ListView
在ListView
的 item 中定义 GlobalObjectKey
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您添加了 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 also simply use the FixedExtentScrollController
for same size items with the index of your initialItem
:您也可以简单地将FixedExtentScrollController
用于具有您的initialItem
索引的相同大小的项目:
controller: FixedExtentScrollController(initialItem: itemIndex);
The documentation : Creates a scroll controller for scrollables whose items have the same size.文档:为项目具有相同大小的可滚动项创建一个滚动控制器。
The simplest way is to call this method inside your InitState method.最简单的方法是在 InitState 方法中调用此方法。 (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. WidgetsBinding.instance.addPostFrameCallback
将保证构建列表并自动搜索您的目标并将滚动条移动到它。 You can then customize the animation of the scroll effect on the Scrollable.ensureVisible
method然后可以在Scrollable.ensureVisible
方法上自定义滚动效果的animation
Note: Remember to add the targetKey
(a GlobalKey
) to the widget you want to scroll to.注意:请记住将targetKey
(一个GlobalKey
)添加到您要滚动到的小部件。
Adding with Rémi Rousselet's answer,加上 Rémi Rousselet 的回答,
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).在这种情况下,请等待键盘完成动画(ios 为 500 毫秒)。
BuildContext context = key.currentContext;
Future.delayed(const Duration(milliseconds: 650), () {
Scrollable.of(context).position.ensureVisible(
context.findRenderObject(),
duration: const Duration(milliseconds: 600));
});
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);
}
This solution improves upon other answers as it does not require hard-coding each elements' heights.该解决方案改进了其他答案,因为它不需要对每个元素的高度进行硬编码。 Adding ScrollPosition.viewportDimension<\/code> and
ScrollPosition.maxScrollExtent<\/code> yields the full content height.
添加
ScrollPosition.viewportDimension<\/code>和
ScrollPosition.maxScrollExtent<\/code>会产生完整的内容高度。
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,
);
这些答案不会使您的列表默认集中在底部,我使用了这个技巧希望它有所帮助:首先反转您提供给ListView.builder
的列表,然后将此属性添加到您的 ListView : reverse: true
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",),
您可以在加载完成后使用 controller.jumpTo(100)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.