跳转至

滚动

一屏幕显示不下,需要上下滚动。

滚动条

body: Scrollbar(
    child: ListView.builder(itemBuilder: itemBuilder),
),

ListView

数据很多的时候,ListView可以动态加载,Column是全部加载。

屏幕能显示多少条数据,只加载显示的。滑出去的回收,新的进来,复用之前的组件。

1、数据固定写死的

children是所有的都进来显示。

ListView(
  children: [
    listViewSection('现金'),
    DiscoverCell(
      imageName: defaultImageName,
      title: '现金',
      onTapCallBack: () {},
    ),
    listViewSection('网络'),
    DiscoverCell(
      title: '微信',
      imageName: defaultImageName,
      onTapCallBack: () {},
    ),
    lineWidget(),
    DiscoverCell(
      title: '支付宝',
      imageName: defaultImageName,
      onTapCallBack: () {},
    ),
  ],
),

2、数据和UI分离

builder是显示多少加载多少。

//构造方法
ListView.builder(
  //重要‼️:根据ListView的内容大小来展示,有多少内容,ListView就多大。当item很少占不满一屏,滑动会超出ListView而看不到。
    shrinkWrap: true,
  //itemBuilder是一个回调函数:Function(BuildContext context, int index); 
  //方法返回一个widget 返回每一个item,类似iOS中tableView的cellForRow,鼠标放上去根据提示快速创建createMethod方法
  itemBuilder: _itemForRow, 
  //总共有多少个item
  itemCount: datas.length,
  //cacheExtent: 10, //缓冲区域大小
  //itemExtent: 60,//主轴方向item的高度 最大60 最小60
  scrollDirection: Axis.horizontal, //横向滚动
);

//定义方法,返回一个widget
//dart中不希望外界访问的话加下划线 下划线的内部指文件内部。整个文件都可以访问,其它文件不能访问。
Widget _itemForRow(BuildContext context, int index) {
  return Container(
    ///省略代码
  );
}

shrinkWrap

ListViewshrinkWrap 属性是一个布尔类型的值,它决定了ListView的主轴(通常是垂直方向)的大小是应该固定还是动态变化。

默认情况下,shrinkWrap 的值是 false,这意味着 ListView 会尽可能的占用其父容器在主轴方向上的所有可用空间。这种情况下,ListView 的大小不受其内容的大小影响,因此它会创建一个具有无限滚动空间的视图。

shrinkWrap 设置为 true 时,ListView 的大小会被内容撑开,也就是说它的大小会根据列表中的项目数量和大小来确定。这样的 ListView 只会占用其内容所需的空间大小,不会占用额外的空间。这在某些布局场景中非常有用,比如当你想要将 ListView 嵌入到一个复杂的布局中,并且你希望 ListView 不要占用比它的内容更多的空间。

需要注意的是,使用 shrinkWrap: true 可能会带来性能上的影响,因为Flutter需要计算ListView中所有子项的大小来确定ListView的大小,这在有很多子项的情况下可能会造成性能问题。因此,只有在确实需要的情况下才设置 shrinkWraptrue,并且要注意其对性能可能产生的影响。

分割线separated

ListView.separated(
    //分割线
    separatorBuilder: (context, index) {
      return const Divider(thickness: 4,);
    },
    itemBuilder: _itemForRow, //itemBuilder是一个回调,方法返回一个widget 相当于cell
    itemCount: datas.length,
),

空数据页面

Container(
  child: _datas.isEmpty
  ? const Center(
    child: Text('loading...'),
  )
  : ListView.builder(
    itemCount: _datas.length,
    itemBuilder: _itemBuilderForRow,
  ),
),

controller

listView滚动的时候需要一个controller属性

点击导航栏文字,跳转到顶部。

final _controller = ScrollController();

return Scaffold(
  appBar: AppBar(
    title: GestureDetector(
      onTap: (){
        //_controller.jumpTo(0.0);
        //动画
        _controller.animateTo(
          -20.0,
          duration: Duration(seconds: 1),
          curve: Curves.linear,
        );
      },
      child: Text("Demo"),
    ),
  ),
  body: Scrollbar(
    child: ListView.builder(
      itemBuilder: _itemForRow, //itemBuilder是一个回调,方法返回一个widget 相当于cell
      itemCount: datas.length,
      scrollDirection: Axis.horizontal, //横向滚动
    ),
  ),
);

现在的位置_controller.offset;

ListView滚动

item的高度 必须要提前知道位置是多少,根据数据内容计算。

ListView滚动的时候会产生一个事件,这个事件可以被监听到。

NotificationListener(
    onNotification: (ScrollNotification _event) {
      print(_event);
      return false;
    },
    child: RefreshIndicator(
      //下拉刷新
      onRefresh: () async {
        await Future.delayed(Duration(seconds: 2)); //网络加载数据
        setState(() {});
      },
      child: ListView.builder(
        itemCount: 200,
        controller: _controller,
        itemBuilder: (_, index) {
          return Container(
            color: Colors.blue[index % 9 * 100],
            height: 50,
          );
        },
      ),
    ),
  )

滚动方向

scrollDirection

滑动删除Dismissible

ListView.builder(
  itemCount: 200,
  itemBuilder: (_, index) {
    return Dismissible(
      key: UniqueKey(),
      //滑动方向
      direction: DismissDirection.endToStart,
      //一定要有key,要知道哪一个被滑走
      background: Container(
        color: Colors.green,
        alignment: Alignment.centerLeft,
        padding: const EdgeInsets.only(left: 24),
        child: const Icon(
          Icons.phone,
          size: 24,
        ),
      ),
      secondaryBackground: Container(
        color: Colors.black,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.only(right: 24),
        child: const Icon(
          Icons.sms,
          color: Colors.white,
        ),
      ),
      onDismissed: (direction) {
        //左滑右滑
        print(direction);
        //UI上删除了,业务逻辑的数据上也得删除
        if (direction == DismissDirection.startToEnd) {
          print("phone");
        }
      },
      //删除之后的动作
      onResize: () {
        print("onResize");
        //删除item,页面变化时会调
      },
      confirmDismiss: (direction) async {
        //是否确认删除
        //可以弹出确认弹窗
        return true;
      },
      //默认超过40%就会滑完
      dismissThresholds: const {
        DismissDirection.startToEnd: 0.1,
        DismissDirection.endToStart: 0.99,
      },
      //滑动方向
      direction: DismissDirection.vertical,
      //删除时间
      resizeDuration: const Duration(seconds: 3),
      movementDuration: const Duration(seconds: 3),
      child: Container(
        height: 72,
        color: Colors.blue[index % 9 * 100],
      ),
    );
  },
)

自定义左滑显示多个按钮

因为Dismissible会在动作完成后移除小部件,所以使用Stack小部件来叠加内容滑动背景。使用ListTile配合GestureDetector或者InkWell来检测滑动手势,并使用AnimatedContainerAnimatedPositioned来控制滑动效果。

如果滑动的偏移量超过一定值,固定按钮的位置,否则重置。

class SlideItem extends StatefulWidget {
  final Widget itemWidget;
  final VoidCallback? callbackAction;
  final VoidCallback deleteAction;
  final VoidCallback editAction;

  const SlideItem(
      {super.key,
      required this.itemWidget,
      this.callbackAction,
      required this.deleteAction,
      required this.editAction});

  @override
  State<SlideItem> createState() => _SlideItemState();
}

class _SlideItemState extends State<SlideItem> {
  double _offset = 0;

  void _slide(double dx) {
    setState(() {
      double newOffset = _offset - dx;
      _offset = newOffset > 150 ? 150 : newOffset;
    });
  }

  void _setEndState() {
    setState(() {
      _offset < 75 ? _offset = 0 : _offset = 150;
    });
  }

  void _reset() {
    setState(() {
      _offset = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 55,
      child: Stack(
        children: <Widget>[
          //底部的widget
          Positioned.fill(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: <Widget>[
                IconButton(
                  icon: const Icon(Icons.edit),
                  color: themeColor(context),
                  onPressed: () {
                    _reset();
                    widget.editAction();
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.delete),
                  color: Colors.red,
                  onPressed: () {
                    _reset();
                    widget.deleteAction();
                  },
                ),
              ],
            ),
          ),
          //上层的widget
          AnimatedPositioned(
            duration: const Duration(milliseconds: 200),
            right: _offset,
            left: -_offset,
            top: 0,
            bottom: 0,
            child: GestureDetector(
              onHorizontalDragUpdate: (details) {
                _slide(details.primaryDelta!);
              },
              onHorizontalDragEnd: (details) {
                _setEndState();
              },
              child: GestureDetector(
                onTap: () {
                  _offset > 1 ? _reset() : widget.callbackAction!();
                },
                child: widget.itemWidget,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

ListView嵌套ListView

里面的ListView需要设置

shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),

RefreshIndicator下拉刷新

在Flutter中实现CustomScrollView的下拉刷新功能,通常会使用RefreshIndicator控件包裹CustomScrollViewRefreshIndicator是一个Material组件,它提供了一个下拉刷新的效果。当用户在列表顶部下拉时,会显示一个圆形进度指示器,并且可以触发一个回调函数来实现数据的刷新。

如果想让列表在加载数据时停留在指示器下方,直到数据加载完成,你需要控制列表的滚动位置。

这通常可以通过以下步骤实现:

  1. 创建一个ScrollController来控制ListView的滚动。
  2. 在触发刷新操作时,调整滚动位置使得ListView保持在指示器下方。
  3. 数据加载完成后,再将滚动位置恢复到顶部。

这里有一个简化的例子展示如何实现这个效果:

class MyHomePage extends StatefulWidget {
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(20, (i) => i);
  final ScrollController _scrollController = ScrollController();

  Future<void> _handleRefresh() async {
    // 让ListView停留在一定的位置,显示指示器
    _scrollController.jumpTo(50);
    // 模拟网络加载
    await Future.delayed(Duration(seconds: 2));
    // 加载数据
    setState(() {
      items = List.generate(20, (i) => items.length + i);
    });
    // 数据加载完成后,恢复ListView的位置
    _scrollController.animateTo(
      0,
      duration: Duration(milliseconds: 200),
      curve: Curves.easeIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Custom Refresh Behavior'),
      ),
      body: RefreshIndicator(
        onRefresh: _handleRefresh,
        color: Colors.white, // 指示器的颜色
        backgroundColor: Colors.blue, // 指示器背景颜色
        displacement: 40.0, // 指示器显示时的偏移量
        child: ListView.builder(
          controller: _scrollController,
          itemCount: items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text('Item ${items[index]}'),
            );
          },
        ),
      ),
    );
  }
}
  • 首先使用jumpTo方法将ListView滚动到一个指定的位置,以便在加载时保持指示器可见。
  • 加载完成后,我们使用animateTo方法将ListView平滑滚动回顶部。
  • onRefresh是一个返回Future<void>的回调函数,在用户下拉刷新时被调用。在这个函数中,执行数据加载的异步操作。当操作完成并且Future解析后,刷新指示器会消失,UI也会根据新的数据进行更新。

GridView网格视图

GridView.builder(
    // gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    //   crossAxisCount: 4,
    //   childAspectRatio: 16 / 9,
    //   mainAxisSpacing: 2,
    //   crossAxisSpacing: 4,
    // ), //横着方向4个

    //设置交叉轴方向最大的尺寸
    gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
      maxCrossAxisExtent: 120, //最大不超过120
      childAspectRatio: 16 / 9,
      mainAxisSpacing: 2.0, //主轴方向间隙
      crossAxisSpacing: 4.0, //交叉轴方向间隙
    ),
    itemBuilder: (_, index) => Container(
      color: Colors.blue[index % 8 * 100],
    ),
    //itemCount: _datas.length,
)

ListWheelScrollView

ListWheelScrollView 是 Flutter 中的一个滚动容器,用于实现一个3D效果的滚动列表,类似于iOS的UIPickerView。这个列表中的条目看起来好像是沿着一个圆柱体的表面滚动的,远离中心的条目会变得更小并向后倾斜。

下面是一个基本的ListWheelScrollView的使用示例:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('ListWheelScrollView Example'),
        ),
        body: Center(
          child: ListWheelScrollView(
            itemExtent: 100, // 每个条目的高度
            diameterRatio: 2, // 圆柱的直径与主轴窗口尺寸的比例
            useMagnifier: true, // 是否使用放大镜效果
            magnification: 1.5, // 中心条目放大的倍数
            children: List<Widget>.generate(
              10, // 生成10个条目
              (index) => Card(
                child: Center(
                  child: Text('Item $index'),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

在这个例子中,ListWheelScrollView 创建了一个包含10个条目的滚动列表。每个条目都是一个Card,里面包含一个居中的Text widget。以下是ListWheelScrollView的一些重要属性:

  • itemExtent: 必需的属性,确定每个子widget在主轴上的固定尺寸。
  • diameterRatio: 确定圆柱的直径与主轴窗口尺寸的比例。较小的值可以产生更强的3D效果。
  • useMagnifier: 确定是否在中心条目上使用放大镜效果。
  • magnification: 如果启用了放大镜,这个值确定中心条目放大的倍数。
  • offAxisFraction: 确定圆柱的偏移量,可以让圆柱向左或向右倾斜。
  • onSelectedItemChanged: 当选中的条目发生变化时调用的回调函数。

ListWheelScrollView 还有许多其他属性可以帮助你自定义滚动行为和外观,包括perspective来调整视觉深度效果,以及physics来定义滚动物理特性,如弹簧效果和滚动动态。

使用ListWheelScrollView时,你可以创建一个选择器、日期选择器或其他滚动选择工具。它为用户提供了一种直观且有趣的方式来从列表中选择条目。

ReorderableListView

拖拽移动List顺序

SingleChildScrollView

在Flutter中,如果你使用SingleChildScrollView并设置其滚动方向为横向(scrollDirection: Axis.horizontal),你可以通过监听滚动通知来知道滚动了多少。

这里有一个简单的例子来说明如何实现这一点:

import 'package:flutter/material.dart';

class MyHorizontalScrollingWidget extends StatefulWidget {
  @override
  _MyHorizontalScrollingWidgetState createState() => _MyHorizontalScrollingWidgetState();
}

class _MyHorizontalScrollingWidgetState extends State<MyHorizontalScrollingWidget> {
  ScrollController _scrollController = new ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
  }

  void _scrollListener() {
    print(_scrollController.position.pixels); // 打印滚动的像素值
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      controller: _scrollController,
      child: Row(
        children: List.generate(20, (index) => Container(
          width: 150,
          height: 100,
          color: index % 2 == 0 ? Colors.blue : Colors.red,
          child: Center(child: Text('Item $index')),
        )),
      ),
    );
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    super.dispose();
  }
}

在这个例子中,创建了一个SingleChildScrollView,它的scrollDirection属性被设置为Axis.horizontal,使其可以横向滚动。

同时使用了一个ScrollController来监听滚动事件,并在_scrollListener方法中打印了当前滚动的位置(以像素为单位)。

记得在dispose方法中移除监听器并且释放ScrollController资源,以防内存泄漏。

当用户滚动时,_scrollListener方法会被触发,并且滚动的像素值会被打印出来。这样就可以知道用户滚动了多少。

SliverPersistentHeader

组头,滑动会固定在顶部。

SliverToBoxAdapter

组头,跟随页面一起滚动。