颤动: 在 ListView 中滚动到一个小部件

如何在 ListView中滚动到一个特殊的小部件? 例如,我想自动滚动到一些 ListView中的 Container,如果我按下一个特定的按钮。

ListView(children: <Widget>[
Container(...),
Container(...), #scroll for example to this container
Container(...)
]);
169640 次浏览

您只需要为您的列表视图指定一个 ScrollController,然后在单击按钮时调用 animateTo方法。

演示 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"),
),
],
),
);
}
}

到目前为止,最简单的解决方案是使用 Scrollable.ensureVisible(context)。因为它可以为您做任何事情,并且可以处理任何尺寸的小部件。使用 GlobalKey获取上下文。

问题是 ListView不会呈现不可见的项目。这意味着你的目标很可能不会构建 完全没有。这意味着您的目标将没有 context; 阻止您在没有更多工作的情况下使用该方法。

最后,最简单的解决方案将取代您的 ListViewSingleChildScrollView和包装您的孩子成为一个 Column。例如:

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"),
),
);
}
}

注意 : 虽然这允许轻松地滚动到所需的项目,但是考虑这种方法只适用于小的预定义列表。至于更大的列表,你会得到性能问题。

但是使 Scrollable.ensureVisibleListView一起工作是可能的,尽管这需要更多的工作。

加载完成后,可以使用 Controler.jumpTo (100)

截图(固定高度内容)

enter image description here


如果项目具有固定的高度,则可以使用以下方法。

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')),
),
);
},
),
);
}
}

不幸的是,ListView 没有 scrollToIndex ()函数的内置方法。你必须开发自己的方法来测量 animateTo()jumpTo()的元素偏移量,或者你可以通过这些建议的解决方案/插件或者从其他文章如 翻动列表视图滚动到索引不可用中搜索

(自2017年以来,颤动/问题/12319一直在讨论 scrollToIndex 问题,但目前仍没有计划)


但是有一种不同的 ListView 支持 scrollToIndex:

你可以像 ListView 一样设置它,它的工作原理也是一样的,只不过你现在可以访问一个 ItemScrollController,它可以:

  • jumpTo({index, alignment})
  • scrollTo({index, alignment, duration, curve})

简单的例子:

ItemScrollController _scrollController = ItemScrollController();


ScrollablePositionedList.builder(
itemScrollController: _scrollController,
itemCount: _myList.length,
itemBuilder: (context, index) {
return _myList[index];
},
)


_scrollController.scrollTo(index: 150, duration: Duration(seconds: 1));

请不要告诉我们,尽管这个 scrollable_positioned_list软件包是由 Google.dev 发布的,但是他们明确声明他们的软件包不是官方支持的 Google 产品。-来源

我找到了一个 太好了解决方案,使用 ListView
我忘记了解决方案是从哪里来的,所以我发布了我的代码。这个功劳属于另一个人。

21/09/22: 编辑。我在这里发布了一个完整的例子,希望它能更清晰。

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),
);


}


}

因为人们正在尝试跳转到小部件在 CustomScrollView。 首先,将此 插件添加到项目中。

然后看看下面的示例代码:

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,
)),
])),
],
),
);
}
}

是的,这只是一个例子,用你的大脑把这个想法变成现实 enter image description here

产出:

使用依赖性:

dependencies:
scroll_to_index: ^1.0.6

代码: (Scroll 将始终执行第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);
}
}

再加上 Rémi Rousselet 的回答,

如果有一种情况,你需要滚动过去,以结束滚动位置,另外键盘弹出,这可能是隐藏的 键盘。你也可能会注意到滚动 当键盘弹出时,动画有点不一致(当键盘弹出时会有附加动画) ,有时会表现得很奇怪。在这种情况下,等待直到键盘完成动画(500ms 为 ios)。

BuildContext context = key.currentContext;
Future.delayed(const Duration(milliseconds: 650), () {
Scrollable.of(context).position.ensureVisible(
context.findRenderObject(),
duration: const Duration(milliseconds: 600));
});

如果您想使小部件可见,以下是 StatefulWidget的解决方案 就在构建视图树之后

通过扩展 雷米的答案,您可以通过以下代码实现:

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;
}
}


  1. 在项目列表中的特定索引处实现初始滚动
  2. 点击浮动动作按钮,你将滚动到一个索引10的项目列表
    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);
}

只需使用页面视图控制器。 例如:

   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",),


       

我在这里张贴的解决方案,其中列表视图将滚动100像素左右。您可以根据您的要求更改价值。它可能有助于那些希望向两个方向滚动列表的人

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),
),
],
),
);
}
}

这个解决方案改进了其他答案,因为它不需要硬编码每个元素的高度。添加 ScrollPosition.viewportDimensionScrollPosition.maxScrollExtent会生成完整的内容高度。这可以用来估计元素在某个索引处的位置。如果所有元素都是相同的高度,则估计是完美的。

// 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,
);

举个完整的例子:

user clicks a button to scroll to the one hundredth element of a long list

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,
),
)
],
),
);
}
}


您也可以简单地使用 FixedExtentScrollController的相同大小的项目与您的 initialItem的索引:

controller: FixedExtentScrollController(initialItem: itemIndex);

文档: 为条目大小相同的滚动条创建滚动控制器。

可以使用 GlobalKey 访问 buildercontext。

我使用 GlobalObjectKeyScrollable

ListView中定义 GlobalObjectKey

ListView.builder(
itemCount: category.length,
itemBuilder: (_, int index) {
return Container(
key: GlobalObjectKey(category[index].id),

您可以从任何地方导航到项

InkWell(
onTap: () {
Scrollable.ensureVisible(GlobalObjectKey(category?.id).currentContext);

添加 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);

最简单的方法是在 InitState 方法内调用此方法(而不是用于清除不需要的错误的构建)

WidgetsBinding.instance.addPostFrameCallback((_) => Scrollable.ensureVisible(targetKey.currentContext!))

WidgetsBinding.instance.addPostFrameCallback将保证构建列表并自动搜索目标并将滚动条移动到目标上。然后可以在 Scrollable.ensureVisible方法上自定义滚动效果的动画

注意: 记住将 targetKey(一个 GlobalKey)添加到要滚动到的小部件中。