Flutter ListView滑动

news2024/11/15 8:01:58

        在Flutter中,ScrollController可以精确地控制和管理滚动行为。通过ScrollController,可以监听滚动的位置、速度,甚至可以在用户滚动时触发自定义的动作。此外,ScrollController还提供了对滚动位置的直接控制,可以编程地滚动到特定位置。

 ScrollController构造函数

 ScrollController({
    double initialScrollOffset = 0.0,
    this.keepScrollOffset = true,
    this.debugLabel,
    this.onAttach,
    this.onDetach,
  }) : _initialScrollOffset = initialScrollOffset {
    if (kFlutterMemoryAllocationsEnabled) {
      ChangeNotifier.maybeDispatchObjectCreation(this);
    }
  }

 ScrollController常用的属性和方法

  • offset:可滚动组件当前的滚动位置。
  • jumpTo(double offset)、animateTo(double offset, {required Duration duration, required Curve curve,}):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。

滚动监听

ScrollController间接继承自Listenable,可以根据ScrollController来监听滚动事件

  final ScrollController _scrollController = ScrollController();
 @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      
    });
  }

 实例

创建一个ListView,当滚动位置发生变化时,记录滑动位置:

final ScrollController _scrollController = ScrollController();

@override
  void initState() {
    super.initState();

    _scrollController.addListener(() {
      print("location:${_scrollController.offset}");
      setState(() {
        offset = _scrollController.offset;
      });
    });
  }


body: ListView(
        scrollDirection: scrollDirection,
        controller: _scrollController,
        children: randomList.map<Widget>((data) {
          return Padding(
            padding: const EdgeInsets.all(8),
            child: _getRow(data[0], math.max(data[1].toDouble(), 50.0)),
          );
        }).toList(),
      )


   TextButton(
          onPressed: () {
  
          },
          child: Text("position: ${offset.floor()}"),
        )

       

 通过animateTo方法回到顶部。

      TextButton(
  onPressed: () {
	_scrollController.animateTo(0,
		duration: const Duration(seconds: 1),//动画时间是1秒,
		curve: Curves.bounceInOut);//动画曲线
  },
  child: Text("回到顶部"),
)

滚动位置恢复 (copy 6.4 滚动监听及控制 | 《Flutter实战·第二版》 (flutterchina.club))

PageStorage是一个用于保存页面(路由)相关数据的组件,它并不会影响子树的UI外观,其实,PageStorage是一个功能型组件,它拥有一个存储桶(bucket),子树中的Widget可以通过指定不同的PageStorageKey来存储各自的数据或状态。

每次滚动结束,可滚动组件都会将滚动位置offset存储到PageStorage中,当可滚动组件重新创建时再恢复。如果ScrollController.keepScrollOffset为false,则滚动位置将不会被存储,可滚动组件重新创建时会使用ScrollController.initialScrollOffset;ScrollController.keepScrollOffset为true时,可滚动组件在第一次创建时,会滚动到initialScrollOffset处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,而initialScrollOffset会被忽略。

当一个路由中包含多个可滚动组件时,如果你发现在进行一些跳转或切换操作后,滚动位置不能正确恢复,这时你可以通过显式指定PageStorageKey来分别跟踪不同的可滚动组件的位置,如:

ListView(key: PageStorageKey(1), ... );
...
ListView(key: PageStorageKey(2), ... );

 不同的PageStorageKey,需要不同的值,这样才可以为不同可滚动组件保存其滚动位置。

注意:一个路由中包含多个可滚动组件时,如果要分别跟踪它们的滚动位置,并非一定就得给他们分别提供PageStorageKey。这是因为Scrollable本身是一个StatefulWidget,它的状态中也会保存当前滚动位置,所以,只要可滚动组件本身没有被从树上移除(detach),那么其State就不会销毁(dispose),滚动位置就不会丢失。只有当Widget发生结构变化,导致可滚动组件的State销毁或重新构建时才会丢失状态,这种情况就需要显式指定PageStorageKey,通过PageStorage来存储滚动位置,一个典型的场景是在使用TabBarView时,在Tab发生切换时,Tab页中的可滚动组件的State就会销毁,这时如果想恢复滚动位置就需要指定PageStorageKey。

ScrollPosition (copy 6.4 滚动监听及控制 | 《Flutter实战·第二版》 (flutterchina.club))

ScrollPosition是用来保存可滚动组件的滚动位置的。一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的positions属性中(List<ScrollPosition>)。

  final List<ScrollPosition> _positions = <ScrollPosition>[];

ScrollPosition是真正保存滑动位置信息的对象,offset只是一个便捷属性:

  double get offset => position.pixels;

一个ScrollController虽然可以对应多个可滚动组件,但是有一些操作,如读取滚动位置offset,则需要一对一!但是我们仍然可以在一对多的情况下,通过其他方法读取滚动位置,举个例子,假设一个ScrollController同时被两个可滚动组件使用,那么我们可以通过如下方式分别读取他们的滚动位置:

...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...    

 我们可以通过controller.positions.length来确定controller被几个可滚动组件使用。

ScrollPosition有两个常用方法:animateTo() 和 jumpTo(),它们是真正来控制跳转滚动位置的方法,ScrollController的这两个同名方法,内部最终都会调用ScrollPosition的。

#ScrollPosition  
@override
  Future<void> animateTo(
    double to, {
    required Duration duration,
    required Curve curve,
  });


#ScrollPosition  
  @override
  void jumpTo(double value);

#ScrollController  
Future<void> animateTo(
    double offset, {
    required Duration duration,
    required Curve curve,
  }) async {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
    await Future.wait<void>(<Future<void>>[
      for (int i = 0; i < _positions.length; i += 1) _positions[i].animateTo(offset, duration: duration, curve: curve),
    ]);
  }

#ScrollController 
  void jumpTo(double value) {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
    for (final ScrollPosition position in List<ScrollPosition>.of(_positions)) {
      position.jumpTo(value);
    }
  }

ScrollController控制原理 (copy 6.4 滚动监听及控制 | 《Flutter实战·第二版》 (flutterchina.club))

ScrollController的三个重要方法:

ScrollPosition createScrollPosition(
    ScrollPhysics physics,
    ScrollContext context,
    ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;

当ScrollController和可滚动组件关联时,可滚动组件首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo() 和 jumpTo()才可以被调用。

当可滚动组件销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo() 和 jumpTo() 将不能再被调用。

需要注意的是,ScrollController的animateTo() 和 jumpTo()内部会调用所有ScrollPosition的animateTo() 和 jumpTo(),以实现所有和该ScrollController关联的可滚动组件都滚动到指定的位置。

滚动监听

  String notify = "";
 @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ScrollToIndexDemoPage"),
      ),
      body: NotificationListener(
          onNotification: (dynamic notification) {
            String notify = "";
            if (notification is ScrollEndNotification) {
              notify = "ScrollEnd";
            } else if (notification is ScrollStartNotification) {
              notify = "ScrollStart";
            } else if (notification is UserScrollNotification) {
              notify = " UserScroll";
            } else if (notification is ScrollUpdateNotification) {
              notify = "ScrollUpdate";
            }
            setState(() {
              this.notify = notify;
            });
            return false;
          },
          child: ListView(
            scrollDirection: scrollDirection,
            controller: _scrollController,
            children: randomList.map<Widget>((data) {
              return Padding(
                padding: const EdgeInsets.all(8),
                child: _getRow(data[0], math.max(data[1].toDouble(), 50.0)),
              );
            }).toList(),
          )),
      persistentFooterButtons: <Widget>[

        const SizedBox(width: 0.3, height: 30.0),
        TextButton(
          onPressed: () {},
          child: Text(notify),
        )
      ],
    );
  }

 ScrollNotification是滚动事件通知,ScrollEndNotification、ScrollStartNotification、UserScrollNotification、ScrollUpdateNotification都是它的子类。

abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
  /// Initializes fields for subclasses.
  ScrollNotification({
    required this.metrics,
    required this.context,
  });

  /// A description of a [Scrollable]'s contents, useful for modeling the state
  /// of its viewport.
  final ScrollMetrics metrics;

  /// The build context of the widget that fired this notification.
  ///
  /// This can be used to find the scrollable's render objects to determine the
  /// size of the viewport, for instance.
  final BuildContext? context;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$metrics');
  }
}
mixin ScrollMetrics {
  /// Creates a [ScrollMetrics] that has the same properties as this object.
  ///
  /// This is useful if this object is mutable, but you want to get a snapshot
  /// of the current state.
  ///
  /// The named arguments allow the values to be adjusted in the process. This
  /// is useful to examine hypothetical situations, for example "would applying
  /// this delta unmodified take the position [outOfRange]?".
  ScrollMetrics copyWith({
    double? minScrollExtent,
    double? maxScrollExtent,
    double? pixels,
    double? viewportDimension,
    AxisDirection? axisDirection,
    double? devicePixelRatio,
  }) {
    return FixedScrollMetrics(
      minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
      maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
      pixels: pixels ?? (hasPixels ? this.pixels : null),
      viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
      axisDirection: axisDirection ?? this.axisDirection,
      devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
    );
  }

 pixels:当前滚动位置。
maxScrollExtent:最大可滚动长度。
extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
atEdge:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)。

虽然 Flutter 官方提供了 ScrollController,调用相关方法可以滚动到指定偏移处,但是官方没有提供滚动到指定下标位置的功能。

scroll_to_index

我们可以使用三方库实现动到指定下标位置的功能。

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';


class ScrollToIndexDemoPage extends StatefulWidget {
  const ScrollToIndexDemoPage({super.key});

  @override
  _ScrollToIndexDemoPageState createState() => _ScrollToIndexDemoPageState();
}

class _ScrollToIndexDemoPageState extends State<ScrollToIndexDemoPage> {
  static const maxCount = 100;

  /// pub  scroll_to_index 项目的 controller
  AutoScrollController? controller;

  final ScrollController _scrollController = ScrollController();

  final random = math.Random();

  final scrollDirection = Axis.vertical;

  late List<List<int>> randomList;

  //双获取偏移=>位置.像素;
  double offset = 0;
  String notify = "";

  @override
  void initState() {
    super.initState();
    controller = AutoScrollController(
        viewportBoundaryGetter: () =>
            Rect.fromLTRB(0, 0, 0, MediaQuery.paddingOf(context).bottom),
        axis: scrollDirection);

    ///一个 index 和 item 高度的数组
    randomList = List.generate(maxCount,
        (index) => <int>[index, (1000 * random.nextDouble()).toInt()]);

    _scrollController.addListener(() {
      print("location:${_scrollController.offset}");
      setState(() {
        offset = _scrollController.offset;
      });
    });
  }

  Widget _getRow(int index, double height) {
    return _wrapScrollTag(
        index: index,
        child: Container(
          padding: const EdgeInsets.all(8),
          alignment: Alignment.topCenter,
          height: height,
          decoration: BoxDecoration(
              border: Border.all(color: Colors.lightBlue, width: 4),
              borderRadius: BorderRadius.circular(12)),
          child: Text('index: $index, height: $height'),
        ));
  }

  Widget _wrapScrollTag({required int index, required Widget child}) =>
      AutoScrollTag(
        key: ValueKey(index),
        controller: controller!,
        index: index,
        highlightColor: Colors.black.withOpacity(0.1),
        child: child,
      );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ScrollToIndexDemoPage"),
      ),
      body: NotificationListener(
          onNotification: (dynamic notification) {
            String notify = "";
            if (notification is ScrollEndNotification) {
              notify = "ScrollEnd";
            } else if (notification is ScrollStartNotification) {
              notify = "ScrollStart";
            } else if (notification is UserScrollNotification) {
              notify = " UserScroll";
            } else if (notification is ScrollUpdateNotification) {
              notify = "ScrollUpdate";
            }
            setState(() {
              this.notify = notify;
            });
            return false;
          },
          child: ListView(
            scrollDirection: scrollDirection,
            controller: controller,
            children: randomList.map<Widget>((data) {
              return Padding(
                padding: const EdgeInsets.all(8),
                child: _getRow(data[0], math.max(data[1].toDouble(), 50.0)),
              );
            }).toList(),
          )),
      persistentFooterButtons: <Widget>[
        TextButton(
          onPressed: () async {
            ///滑动到第13个的位置
            await controller!
                .scrollToIndex(13, preferPosition: AutoScrollPosition.begin);
            controller!.highlight(13);
          },
          child: const Text("Scroll to 13"),
        ),
        // const SizedBox(width: 0.3, height: 30.0),
        // TextButton(
        //   onPressed: () {
        //     // _scrollController.animateTo(0,
        //     //     duration: const Duration(seconds: 1),
        //     //     curve: Curves.bounceInOut);
        //     // // setState(() {
        //     // //   entries = entries;
        //     // // });
        //   },
        //   child: Text("position: ${offset.floor()}"),
        // ),
        // const SizedBox(width: 0.3, height: 30.0),
        // TextButton(
        //   onPressed: () {
        //     _scrollController.animateTo(0,
        //         duration: const Duration(seconds: 1),
        //         curve: Curves.bounceInOut);
        //   },
        //   child: Text("回到顶部"),
        // ),

        // const SizedBox(width: 0.3, height: 30.0),
        // TextButton(
        //   onPressed: () {},
        //   child: Text(notify),
        // )
      ],
    );
  }
}

AutoScrollTag 是一个可以包裹在任意行级 widget 中的组件,它会接收控制器和索引值,并在需要时高亮显示。
AutoScrollController 则负责整个滚动操作,包括监听和触发滚动到指定索引的命令。

对于有固定行高的情况,可以设置 suggestedRowHeight 参数以提高滚动效率。
通过 viewportBoundaryGetter 自定义视口边界,以及选择垂直或水平滚动方向。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2074630.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Aiseesoft PDF Converter Ultimate 3.3.62 + crack

Aiseesoft PDF Converter Ultimate 是一个用于将 PDF 文件转换为其他格式的程序。它允许您将 PDF 转换为 Word、Text、Excel、PowerPoint、ePub、HTML、JPG、TIFF、PNG、GIF 等。 以下是该程序主要功能的简要说明: 支持不同的格式。该程序支持超过 25 种不同的格式,用于从 P…

PyTorch深度学习网络(一:MLP)

全连接神经网络&#xff0c;又称多层感知机&#xff08;MLP&#xff09;&#xff0c;是深度学习最基础的神经网络。全连接神经网络主要由输入层、隐藏层和输出层构成。本文实现了一个通用MLP网络&#xff0c;包括以下功能&#xff1a; 根据输入的特征数、类别数、各隐藏层神经…

Canvas 动画: atan2 三角函数与鼠标跟随效果

这个案例展示了如何使用HTML5的Canvas和JavaScript实现一个动态效果&#xff1a;在画布上绘制一个箭头&#xff0c;并让它实时跟随鼠标移动。这个小项目不仅有趣&#xff0c;还能帮助你理解编程和基本数学概念的实际应用。 项目需求 我们的目标是在一个画布上绘制一个箭头&…

java-4 final、单例类、枚举类、抽象类、接口

final 1. 认识final 2. 常量 大项目中经常将常量集中写在Constant文件中 单例类 &#xff08;设计模式&#xff09; 为什么要把构造器私有化&#xff0c;你不是私有化&#xff0c;别人就可以 new 好多个对象&#xff0c;还怎么是单例吖 定义一个类变量、类方法&#xff0c;…

海外媒体宣发:著名媒体【越南通讯社VNanet】发布新闻稿

海外媒体宣发&#xff1a;著名媒体【越南通讯社VNanet】发布新闻稿 近日&#xff0c;越南通讯社VNanet发布了一篇关于全球气候变化的新闻稿&#xff0c;引起了广泛关注。本文将详细介绍新闻稿的主要内容以及其对全球气候变化的影响。 一、新闻稿概述 越南通讯社VNanet作为越…

解决WIndows10下更新蓝牙驱动屡屡失败问题

因为换了个1T自带Win10系统的SSD硬盘&#xff0c;导致蓝牙驱动死活装不上了。驱动精灵&#xff0c;官方驱动都没用。这可前所未闻啊。 想起换下来的硬盘系统里面还有系统在&#xff0c;试试看能不能直接用之前的系统蓝牙驱动&#xff0c;原则上是应该没问题的。所以就将之前的…

混合现实UI优化:利用物理环境的直接交互

随着虚拟现实(VR)和混合现实(MR)技术的发展,用户界面(UI)的设计变得越来越重要,尤其是在需要适应多种物理环境的情况下。本文将介绍一种名为 InteractionAdapt 的用户界面优化方法,它专为VR环境中的工作空间适配而设计,能够有效利用物理环境,为用户提供更加灵活和个…

Kafka的Offset(偏移量)详解

Kafka的Offset详解 1、生产者Offset2、消费者Offset2.1、消费者2.2、生产者2.3、实体类对象2.4、JSON工具类2.5、项目配置文件2.6、测试类2.7、测试2.8、总结 1、生产者Offset 2、消费者Offset 2.1、消费者 package com.power.consumer;import org.apache.kafka.clients.consu…

Android - 自定义view

为什么要自定义view&#xff1f; 在Android开发中有很多业务场景&#xff0c;原生的控件无法满足需求&#xff0c;并且经常也会遇到一个UI在多处重复使用情况&#xff0c;于是可以通过自定义View的方式来实现这些UI效果。 自定义view的分类 自定义属性 Window window是一个…

图数据库查询语言 cypher 与 memgraph

Cyper 作为声明式查询语言, SQL 在计算机行业无人不晓, 无人不知. 而 Cypher 就是 Graph Database 图数据库的 SQL. Cypher 用"圆括号"来表示节点, 用"方括号,连接线及箭头"表示关系 这样一句话 - "Sally likes Graphs. Sally is friends with John. …

完成控制器方法获取参数-@RequestParam

文章目录 1.将方法的request和response参数封装到参数数组1.SunDispatcherServlet.java1.根据方法信息&#xff0c;返回实参列表2.具体调用 2.测试 2.封装Http请求参数到参数数组1.自定义RequestParam注解2.MonsterController.java 增加参数3.SunDispatcherServlet.java1.resol…

软件架构的发展经历了从单体结构、垂直架构、SOA架构到微服务架构的过程剖析

1.单体架构 特点: 1、所有的功能集成在一个项目工程中。 2、所有的功能打一个war包部署到服务器。 3、应用与数据库分开部署。 4、通过部署应用集群和数据库集群来提高系统的性能。 优点: 1、项目架构简单,前期开发成本低,周期短,小型项目的首选。 缺点: 1、全部…

c++实现mysql关系型数据库连接与增删改查操作

最近老师让我实现这个功能&#xff0c;顺便发个东西&#xff0c;我感觉mysql从入门到精通这本书写的蛮好的&#xff0c;其实连接数据库就是调用mysql-c-api库里面的函数mysql_real_connect,下来的增删改查&#xff0c;也无非就是cmd命令台里面的语句&#xff0c;插入&#xff1…

Javaweb学习之Vue实践小界面(四)

目录 前情回顾 本期介绍 效果图 第一步&#xff1a;前期工作 第二步&#xff1a;建立页眉 效果图 第三步&#xff1a;建立导航栏 效果图 第四步&#xff1a;主要内容放置 效果图 第五步&#xff1a;建立页脚 效果图 综合&#xff1a;将文字和背景更改成自己喜欢的…

PEM燃料电池启停控制策略优化的simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 PEM燃料电池启停控制策略优化的simulink建模与仿真。 1.燃料电池提供是燃料转换为电能和热能的装置。 2.功率的输出的改变通过很多因素&#xff0c;如温度&#xff0c;压力…

谷歌、火狐及Edge等浏览器如何使用allWebPlugin中间件响应ActiveX插件事件

allWebPlugin简介 allWebPlugin中间件是一款为用户提供安全、可靠、便捷的浏览器插件服务的中间件产品&#xff0c;致力于将浏览器插件重新应用到所有浏览器。它将现有ActiveX控件直接嵌入浏览器&#xff0c;实现插件加载、界面显示、接口调用、事件回调等。支持Chrome、Firefo…

模型 OGSM(战略规划)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。目标引领&#xff0c;策略驱动&#xff0c;量化衡量。 1 OGSM模型的应用 1.1 电商企业年度增长战略 某电商企业面临激烈的市场竞争&#xff0c;决定运用OGSM模型来规划其年度战略&#xff0c;以实现…

代码随想录Day 25|回溯篇完结,题目:491.递增子序列、46、全排列、47.全排列Ⅱ

提示&#xff1a;DDU&#xff0c;供自己复习使用。欢迎大家前来讨论~ 文章目录 第七章 回溯算法part05一、题目题目一&#xff1a;491.递增子序列解题思路&#xff1a;回溯三部曲优化 题目二&#xff1a;46.全排列[46. 全排列](https://leetcode.cn/problems/permutations/)解…

日撸Java三百行(day34:图的深度优先遍历)

目录 一、深度优先搜索 二、图的深度优先遍历 三、代码实现 总结 一、深度优先搜索 深度优先搜索&#xff08;Depth First Search&#xff1a;DFS&#xff09;是一种用于遍历树或图的算法&#xff0c;具体来说就是从起始节点开始&#xff0c;沿某一分支路径不断深入&#…

Linux内核定时器、阻塞_非阻塞IO

一.内核时间管理 Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率…