Flutter ScrollController滚动监听及控制示例详解
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>ScrollController</li><ul class="second_class_ul"><li>滚动监听</li><li>滚动监听示例</li><li>滚动位置恢复</li><li>ScrollPosition</li><li>ScrollPosition的方法</li><li>ScrollController控制原理</li></ul><li>滚动监听</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>ScrollController</h2><p><code>ScrollController</code>构造函数如下:</p>
<div class="jb51code"><pre class="brush:java;">ScrollController({
double initialScrollOffset = 0.0, //初始滚动位置
this.keepScrollOffset = true,//是否保存滚动位置
...
})
</pre></div>
<p>我们介绍一下ScrollController常用的属性和方法:</p>
<ul><li>offset:可滚动组件当前的滚动位置。</li><li>jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。</li></ul>
<p>ScrollController还有一些属性和方法,我们将在后面原理部分解释。</p>
<p class="maodian"></p><h3>滚动监听</h3>
<p>ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件,如:</p>
<div class="jb51code"><pre class="brush:java;">controller.addListener(()=>print(controller.offset))
</pre></div>
<p class="maodian"></p><h3>滚动监听示例</h3>
<p>我们创建一个<code>ListView</code>,当滚动位置发生变化时,我们先打印出当前滚动位置,然后判断当前位置是否超过1000像素,如果超过则在屏幕右下角显示一个“返回顶部”的按钮,该按钮点击后可以使ListView恢复到初始位置;如果没有超过1000像素,则隐藏“返回顶部”按钮。代码如下:</p>
<div class="jb51code"><pre class="brush:java;">import 'package:demo202112/utils/common_appbar.dart';
import 'package:flutter/material.dart';
/// @Author wywinstonwy
/// @Date 2022/1/19 10:46 下午
/// @Description:
class MyScrollController extends StatefulWidget {
const MyScrollController({Key? key}) : super(key: key);
@override
_MyScrollControllerState createState() => _MyScrollControllerState();
}
class _MyScrollControllerState extends State<MyScrollController> {
final ScrollController _controller = ScrollController();
bool showToTopBtn = false; //是否显示“返回到顶部”按钮
@override
void initState() {
// TODO: implement initState
super.initState();
//监听滚动事件,打印滚动位置
_controller.addListener(() {
//打印滚动位置
print(_controller.offset);
if (_controller.offset < 1000 && showToTopBtn) {
setState(() {
showToTopBtn = false;
});
} else if (_controller.offset >= 1000 && showToTopBtn == false) {
setState(() {
showToTopBtn = true;
});
}
});
}
@override
void dispose() {
//为了避免内存泄露,需要调用_controller.dispose
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar('滚动监听以及控制'),
body: _buildScollbar(),
floatingActionButton: showToTopBtn==false?null:FloatingActionButton(
onPressed: (){
//返回到顶部时候执行动画
_controller.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeIn);
},
child: const Icon(Icons.arrow_upward),),
);
}
_buildScollbar(){
return Scrollbar(
child: ListView.builder(
controller: _controller,
itemCount: 100,
itemExtent: 44,
itemBuilder: (context,index){
return ListTile(title: Text('$index'),);
})
);
}
}
</pre></div>
<p>运行效果:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919028.gif" /></p>
<p>由于列表项高度为 44 像素,当滑动到第 20+ 个列表项后,右下角 “返回顶部” 按钮会显示,点击该按钮,ListView 会在返回顶部的过程中执行一个滚动动画,动画时间是 200 毫秒,动画曲线是 <code>Curves.ease</code>。</p>
<p class="maodian"></p><h3>滚动位置恢复</h3>
<p><code>PageStorage</code>是一个用于保存页面(路由)相关数据的组件,它并不会影响子树的UI外观,其实,<code>PageStorage</code>是一个功能型组件,它拥有一个存储桶(bucket),子树中的Widget可以通过指定不同的<code>PageStorageKey</code>来存储各自的数据或状态。</p>
<p>每次滚动结束,可滚动组件都会将滚动位置<code>offset</code>存储到<code>PageStorage</code>中,当可滚动组件重新创建时再恢复。如果<code>ScrollController.keepScrollOffset</code>为false,则滚动位置将不会被存储,可滚动组件重新创建时会使用<code>ScrollController.initialScrollOffset</code>;<code>ScrollController.keepScrollOffset</code>为true时,可滚动组件在第一次创建时,会滚动到<code>initialScrollOffset</code>处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,而<code>initialScrollOffset</code>会被忽略。</p>
<p>当一个路由中包含多个可滚动组件时,如果你发现在进行一些跳转或切换操作后,滚动位置不能正确恢复,这时你可以通过显式指定<code>PageStorageKey</code>来分别跟踪不同的可滚动组件的位置,如:</p>
<div class="jb51code"><pre class="brush:java;">ListView(key: PageStorageKey(1), ... );
...
ListView(key: PageStorageKey(2), ... );
</pre></div>
<p>不同的<code>PageStorageKey</code>,需要不同的值,这样才可以为不同可滚动组件保存其滚动位置。</p>
<p>注意:一个路由中包含多个可滚动组件时,如果要分别跟踪它们的滚动位置,并非一定就得给他们分别提供PageStorageKey。这是因为Scrollable本身是一个StatefulWidget,它的状态中也会保存当前滚动位置,所以,只要可滚动组件本身没有被从树上detach掉,那么其State就不会销毁(dispose),滚动位置就不会丢失。只有当Widget发生结构变化,导致可滚动组件的State销毁或重新构建时才会丢失状态,这种情况就需要显式指定PageStorageKey,通过PageStorage来存储滚动位置,一个典型的场景是在使用TabBarView时,在Tab发生切换时,Tab页中的可滚动组件的State就会销毁,这时如果想恢复滚动位置就需要指定</p>
<p class="maodian"></p><h3>ScrollPosition</h3>
<p><code>ScrollPosition</code>是用来保存可滚动组件的滚动位置的。一个<code>ScrollController</code>对象可以同时被多个可滚动组件使用,<code>ScrollController</code>会为每一个可滚动组件创建一个<code>ScrollPosition</code>对象,这些<code>ScrollPosition</code>保存在<code>ScrollController</code>的<code>positions</code>属性中(List<ScrollPosition>)。<code>ScrollPosition</code>是真正保存滑动位置信息的对象,<code>offset</code>只是一个便捷属性:</p>
<div class="jb51code"><pre class="brush:java;">double get offset => position.pixels;
</pre></div>
<p>一个<code>ScrollController</code>虽然可以对应多个可滚动组件,但是有一些操作,如读取滚动位置offset,则需要一对一!但是我们仍然可以在一对多的情况下,通过其它方法读取滚动位置,举个例子,假设一个ScrollController同时被两个可滚动组件使用,那么我们可以通过如下方式分别读取他们的滚动位置:</p>
<div class="jb51code"><pre class="brush:java;">...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...
</pre></div>
<p>我们可以通过<code>controller.positions.length</code>来确定<code>controller</code>被几个可滚动组件使用。</p>
<p class="maodian"></p><h3>ScrollPosition的方法</h3>
<p><code>ScrollPosition</code>有两个常用方法:<code>animateTo()</code> 和 <code>jumpTo()</code>,它们是真正来控制跳转滚动位置的方法,<code>ScrollController</code>的这两个同名方法,内部最终都会调用<code>ScrollPosition</code>的。</p>
<p class="maodian"></p><h3>ScrollController控制原理</h3>
<p>我们来介绍一下<code>ScrollController</code>的另外三个方法:</p>
<div class="jb51code"><pre class="brush:java;">ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;
</pre></div>
<p>当<code>ScrollController</code>和可滚动组件关联时,可滚动组件首先会调用<code>ScrollController</code>的<code>createScrollPosition()</code>方法来创建一个<code>ScrollPosition</code>来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的<code>ScrollPosition</code>添加到<code>ScrollController</code>的<code>positions</code>属性中,这一步称为“注册位置”,只有注册后<code>animateTo()</code> 和 <code>jumpTo()</code>才可以被调用。</p>
<p>当可滚动组件销毁时,会调用<code>ScrollController</code>的<code>detach()</code>方法,将其<code>ScrollPosition</code>对象从<code>ScrollController</code>的<code>positions</code>属性中移除,这一步称为“注销位置”,注销后<code>animateTo()</code> 和 <code>jumpTo()</code> 将不能再被调用。</p>
<p>需要注意的是,<code>ScrollController</code>的<code>animateTo()</code> 和 <code>jumpTo()</code>内部会调用所有<code>ScrollPosition</code>的<code>animateTo()</code> 和 <code>jumpTo()</code>,以实现所有和该<code>ScrollController</code>关联的可滚动组件都滚动到指定的位置。</p>
<p class="maodian"></p><h2>滚动监听</h2>
<p>下面,我们监听<code>ListView</code>的滚动通知,然后显示当前滚动进度百分比:</p>
<div class="jb51code"><pre class="brush:java;">import 'package:demo202112/utils/common_appbar.dart';
import 'package:flutter/material.dart';
/// @Author wywinstonwy
/// @Date 2022/1/19 11:21 下午
/// @Description:
class MyScrollcontroller2 extends StatefulWidget {
const MyScrollcontroller2({Key? key}) : super(key: key);
@override
_MyScrollcontroller2State createState() => _MyScrollcontroller2State();
}
class _MyScrollcontroller2State extends State<MyScrollcontroller2> {
String _progress ='0%';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar("滚动监听"),
body: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification){
double progress = notification.metrics.pixels/notification.metrics.maxScrollExtent;
//重新构建
setState(() {
_progress ='${(progress*100).toInt()}%';
});
print("BottomEdge: ${notification.metrics.extentAfter == 0}");
return false; //return true; //放开此行注释后,进度条将失效
},
child: Stack(
alignment: Alignment.center,
children: [
ListView.builder(
itemCount: 100,
itemExtent: 50,
itemBuilder: (context,index){
return ListTile(title: Text('$index'),);
}),
CircleAvatar(
radius: 30,
child: Text(_progress),
backgroundColor: Colors.black54,
)
],
),
),
);
}
}
</pre></div>
<p>运行结果:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919029.gif" /></p>
<p>在接收到滚动事件时,参数类型为<code>ScrollNotification</code>,它包括一个<code>metrics</code>属性,它的类型是<code>ScrollMetrics</code>,该属性包含当前<code>ViewPort</code>及滚动位置等信息:</p>
<ul><li><code>pixels</code>:当前滚动位置。</li><li><code>maxScrollExtent</code>:最大可滚动长度。</li><li><code>extentBefore</code>:滑出<code>ViewPort</code>顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。</li><li><code>extentInside</code>:<code>ViewPort</code>内部长度;此示例中屏幕显示的列表部分的长度。</li><li><code>extentAfter</code>:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。</li><li><code>atEdge</code>:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)。</li></ul>
<p><code>ScrollMetrics</code>还有一些其它属性,可以自行查阅API文档。</p>
<p>详细的官方文档地址:api.flutter.dev/flutter/wid…</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919030.jpg" /></p>
<blockquote><p>官方文档解释 控制可滚动小部件。</p>
<p>滚动控制器通常作为成员变量存储在State对象中,并在每个State.build中重用。单个滚动控制器可用于控制多个可滚动小部件,但有些操作(如读取滚动偏移量)要求控制器与单个可滚动小部件一起使用。</p>
<p>滚动控制器创建一个ScrollPosition来管理特定于单个可滚动小部件的状态。要使用自定义的ScrollPosition,子类化ScrollController并重写createScrollPosition。</p>
<p>ScrollController是一个Listenable。当附加的任何scrollposition通知它们的侦听器时(即当它们中的任何一个滚动时),它会通知它的侦听器。当附加的scrollposition列表发生变化时,它不会通知侦听器。</p>
<p>通常与ListView, GridView, CustomScrollView一起使用。</p>
<p>参见: ListView, GridView, CustomScrollView,它们可以由ScrollController控制。 Scrollable,它是较低层的小部件,用于创建ScrollPosition对象和ScrollController对象并将它们关联起来。 PageController,它是控制PageView的一个类似对象。 ScrollPosition,用于管理单个滚动小部件的滚动偏移量。 ScrollNotification和NotificationListener,它们可用于监视滚动位置,而无需使用ScrollController。</p></blockquote>
<p>以上就是Flutter ScrollController滚动监听及控制示例详解的详细内容,更多关于Flutter ScrollController滚动监听的资料请关注琼殿技术社区其它相关文章!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>Android Flutter利用CustomPaint绘制基本图形详解</li><li>Flutter绘图组件之CustomPaint使用详解</li><li>用Flutter做桌上弹球(绘图(Canvas&CustomPaint)API)</li><li>flutter实现磨砂玻璃效果实例详解</li><li>Flutter CustomPaint自定义绘画示例详解</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]