1.ScrollController
ScrollController({
double initialScrollOffset = 0.0, //初始滚动位置
this.keepScrollOffset = true,//是否保存滚动位置
// 如果ScrollController.keepScrollOffset为false,则滚动位置将不会被存储,Scrollable Widget重新创建时会使用ScrollController.initialScrollOffset;
// ScrollController.keepScrollOffset为true时,Scrollable Widget在第一次创建时,会滚动到initialScrollOffset处,
// 因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,而initialScrollOffset会被忽略
// tab页面切换时候,ListView会保存位置
...
})
2.ScrollController常用的属性和方法
offset
:可滚动Widget当前滚动的位置
jumpTo(double offset)
、animateTo(double offset,...)
:这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会
3.滚动监听
controller.addListener(()=>print(controller.offset))
4.例子:当滚动位置发生变化时,然后判断当前位置是否超过500像素,如果超过则在屏幕右下角显示一个“返回顶部”的按钮,该按钮点击后可以使ListView恢复到初始位置;如果没有超过500像素,则隐藏“返回顶部”按钮
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
class MyList extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _MyList();
}
}
class _MyList extends State<MyList> {
List _list = ["loading"];
//自定义的scrollController
ScrollController _scroll = new ScrollController();
bool isOver = false;
bool hasTop = false; //是否展示返回顶部按钮
@override
void initState() {
super.initState();
_retrieveData();
_scroll.addListener(() {
if (_scroll.position.pixels >= _scroll.position.maxScrollExtent &&
!isOver) {
_retrieveData();
}
//超过500,就显示返回顶部按钮,小于500就不展示
if (_scroll.offset >= 500 && !hasTop) {
setState(() {
hasTop = true;
});
} else if (_scroll.offset < 500 && hasTop) {
setState(() {
hasTop = false;
});
}
});
}
@override
void dispose() {
_scroll.dispose();
super.dispose();
}
Widget _getList() {
return Column(
children: <Widget>[
Expanded(
flex: 1,
child: ListView.builder(
controller: _scroll, //scroll的配置
itemCount: _list.length,
itemExtent: 60.0,
itemBuilder: (context, index) {
if (_list[index] == "loading") {
if (_list.length >= 50) {
isOver = true; //是否已经结束
return Row(
children: <Widget>[Text("没有更多了")],
mainAxisAlignment: MainAxisAlignment.center,
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(strokeWidth: 2.0)),
],
);
}
}
return ListTile(
title: Text(_list[index]),
);
},
),
)
],
);
}
void _retrieveData() {
Future.delayed(Duration(seconds: 2)).then((e) {
setState(() {
_list.insertAll(
_list.length - 1,
//每次生成20个单词
generateWordPairs().take(100).map((e) => e.asPascalCase).toList());
});
});
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("list"),
),
body: _getList(),
//返回顶部按钮
floatingActionButton: hasTop
? FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_scroll.animateTo(.0,
duration: Duration(milliseconds: 300), curve: Curves.ease);
},
)
: null,
);
}
}
5.keepScrollOffset的使用
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
class MyBar extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _MyBar();
}
}
class _MyBar extends State<MyBar> with SingleTickerProviderStateMixin {
List _tabs = ["首页", "新闻", "城市"];
List _tem = [];
List _scrollList = [
new ScrollController(), //默认keepScrollOffset是true,记录滚动位置
new ScrollController(),
new ScrollController()
];
TabController _tb;
void initState() {
super.initState();
_tb = new TabController(length: _tabs.length, vsync: this);
_tem.addAll(
generateWordPairs().take(50).map((e) => e.asPascalCase).toList());
}
List<ListView> _getContent() {
var tabs =new List<ListView>();
for (int i = 0; i < _tabs.length; i++) {
tabs.add(ListView.builder(
key: PageStorageKey(i+1),
controller: _scrollList[i],
itemCount: _tem.length,
itemExtent: 50.0,
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
child: Text("${_tabs[i]}--${index + 1}----${_tem[index]}"),
);
},
));
}
print(tabs);
return tabs.toList();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("tab-list"),
centerTitle: true,
bottom: TabBar(
controller: _tb,
tabs: _tabs
.map((txt) => Tab(
text: txt,
))
.toList()),
),
body: TabBarView(
controller: _tb,
children: _getContent(),
),
);
}
}
首页滑动到新闻,在返回首页的时候,首页会记录之前的位置。
如果首页的ListView的 keepScrollOffset为false,就不会记录位置,在返回首页的时候还是在最开始1的位置
6.NotificationListener
NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑,该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true
时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false
时,则冒泡继续
在接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics
属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息
- pixels:当前滚动位置。
- maxScrollExtent:最大可滚动长度。
- extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
- extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
- extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
- atEdge:是否滑到了Scrollable Widget的边界(此示例中相当于列表顶或底部)。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class NocaScroll extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _NocaScroll();
}
}
class _NocaScroll extends State<NocaScroll> {
String _progress = "0%";
Widget _getScroll() {
return Scrollbar(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
double pr = notification.metrics.pixels /
notification.metrics.maxScrollExtent;
setState(() {
_progress = "${(pr * 100).toInt()}%";
});
// return true 如果这行展示,就不会出现滚动条
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, i) {
return ListTile(
title: Text("list$i"),
);
},
),
CircleAvatar(
radius: 50.0,
backgroundColor: Colors.grey,
child: Text(_progress),
)
],
),
),
);
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("NocaScroll"),
),
body: _getScroll(),
);
}
}