Flutter双向滚动插件two_way_scrollable的使用

Flutter双向滚动插件two_way_scrollable的使用

插件简介

two_way_scrollable 是一个Flutter插件,提供了一组可以在两个方向上扩展的可滚动组件。这些组件可以正确地填充视口,即使内容不足。它特别适用于需要在顶部和底部同时添加或删除项目的场景。

主要特性

  • TwoWayCustomScrollView:这是一个 CustomScrollView 的替代品,支持在两个方向上扩展。
  • TwoWayListView:这是一个基于 TwoWayCustomScrollViewSliverTwoWayListAnimatedListView 类似物,支持在两个方向上扩展。
  • SliverTwoWayList:一组 Sliver 组件,可以与 TwoWayCustomScrollViewCustomScrollView 一起使用,以实现列表中的任意定位。

注意事项

  • TwoWayCustomScrollViewTwoWayListView 只允许锚定到顶部或底部,但不能锚定到中间。

安装

pubspec.yaml 文件中添加以下依赖:

dependencies:
  two_way_scrollable: ^latest_version

然后运行以下命令来安装插件:

flutter pub add two_way_scrollable

使用示例

下面是一个完整的示例代码,展示了如何使用 two_way_scrollable 插件创建一个双向滚动的列表。这个示例包括了添加、删除项目以及切换滚动方向的功能。

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:two_way_scrollable/two_way_scrollable.dart';
import 'package:random_color/random_color.dart';

void main() {
  runApp(const SandboxApp());
}

class SandboxApp extends StatelessWidget {
  const SandboxApp({
    super.key,
    this.anchor = TwoWayListViewAnchor.top,
    this.direction = TwoWayListViewDirection.topToBottom,
  });

  final TwoWayListViewAnchor anchor;
  final TwoWayListViewDirection direction;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: _Content(
        anchor: anchor,
        direction: direction,
      ),
    );
  }
}

class _Content extends StatefulWidget {
  const _Content({
    Key? key,
    required this.anchor,
    required this.direction,
  }) : super(key: key);

  final TwoWayListViewAnchor anchor;
  final TwoWayListViewDirection direction;

  [@override](/user/override)
  State<_Content> createState() => _ContentState();
}

class _ContentState extends State<_Content> {
  var ctrl = TwoWayListController<int>();

  late var anchor = widget.anchor;
  late var direction = widget.direction;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: buildAppBar(),
      body: RepaintBoundary(
        child: Stack(
          key: const Key('TwoWayListView'),
          children: [
            Container(color: Colors.white),
            buildListView(),
          ],
        ),
      ),
    );
  }

  AppBar buildAppBar() {
    return AppBar(
      centerTitle: false,
      actions: [
        // 添加项目(顶部)
        InkResponse(
          key: const ValueKey('add-first'),
          onTap: () {
            final first = ctrl.items.firstOrNull;
            ctrl.insert(-1, first != null ? first - 1 : -1);
          },
          onLongPress: () {
            final first = ctrl.items.firstOrNull ?? 0;
            final items = List.generate(10, (i) => first - i - 1);
            ctrl.insertAll(-1, items.reversed.toList());
          },
          child: const Icon(Icons.arrow_upward),
        ),
        // 添加项目(底部)
        InkResponse(
          key: const ValueKey('add-last'),
          onTap: () {
            final last = ctrl.items.lastOrNull;
            ctrl.insert(ctrl.items.length, last != null ? last + 1 : 0);
          },
          onLongPress: () {
            final last = ctrl.items.lastOrNull ?? -1;
            final items = List.generate(10, (i) => last + i + 1);
            ctrl.insertAll(ctrl.items.length, items);
          },
          child: const Icon(Icons.arrow_downward),
        ),
        // 删除项目(顶部)
        InkResponse(
          key: const ValueKey('remove-first'),
          onTap: () {
            final item = ctrl.items.firstOrNull;
            if (item == null) return;
            ctrl.remove(item);
          },
          onLongPress: () {
            for (var i = 0; i < 10; i++) {
              final item = ctrl.items.firstOrNull;
              if (item == null) return;
              ctrl.remove(item);
            }
          },
          child: const Icon(Icons.arrow_upward),
        ),
        // 删除项目(底部)
        InkResponse(
          key: const ValueKey('remove-last'),
          onTap: () {
            final item = ctrl.items.lastOrNull;
            if (item == null) return;
            ctrl.remove(item);
          },
          onLongPress: () {
            for (var i = 0; i < 10; i++) {
              final item = ctrl.items.lastOrNull;
              if (item == null) return;
              ctrl.remove(item);
            }
          },
          child: const Icon(Icons.arrow_downward),
        ),
        // 切换锚点位置
        InkResponse(
          key: const ValueKey('anchor'),
          onTap: () => setState(() {
            _swapAnchor();
          }),
          child: anchor == TwoWayListViewAnchor.top
              ? const Icon(Icons.vertical_align_top)
              : const Icon(Icons.vertical_align_bottom),
        ),
        // 切换滚动方向
        InkResponse(
          key: const ValueKey('direction'),
          onTap: () => setState(() {
            _swapDirection();
          }),
          child: direction == TwoWayListViewDirection.topToBottom
              ? const Icon(Icons.sort_by_alpha)
              : const Icon(Icons.sort_by_alpha_outlined),
        ),
        // 切换锚点和方向
        InkResponse(
          key: const ValueKey('reverse'),
          onTap: () => setState(() {
            _swapAnchor();
            _swapDirection();
          }),
          child: const Icon(Icons.swap_vert),
        ),
        // 重置
        InkResponse(
          key: const ValueKey('refresh'),
          onTap: () => setState(() {
            ctrl = TwoWayListController<int>();
            direction = TwoWayListViewDirection.topToBottom;
            anchor = TwoWayListViewAnchor.top;
          }),
          child: const Icon(Icons.refresh),
        ),
      ],
    );
  }

  void _swapAnchor() {
    switch (anchor) {
      case TwoWayListViewAnchor.top:
        anchor = TwoWayListViewAnchor.bottom;
        break;
      case TwoWayListViewAnchor.bottom:
        anchor = TwoWayListViewAnchor.top;
        break;
    }
  }

  void _swapDirection() {
    switch (direction) {
      case TwoWayListViewDirection.topToBottom:
        direction = TwoWayListViewDirection.bottomToTop;
        break;
      case TwoWayListViewDirection.bottomToTop:
        direction = TwoWayListViewDirection.topToBottom;
        break;
    }
  }

  Widget buildListView() {
    return TwoWayListView(
      controller: ctrl,
      anchor: anchor,
      direction: direction,
      topSlivers: const [
        SliverToBoxAdapter(
          child: _DebugListBoundaryIndicator(),
        ),
        SliverToBoxAdapter(
          child: SizedBox(height: 16),
        ),
      ],
      centerSliver: const SliverToBoxAdapter(
        child: _DebugCenterIndicator(),
      ),
      bottomSlivers: const [
        SliverToBoxAdapter(
          child: SizedBox(height: 16),
        ),
        SliverToBoxAdapter(
          child: _DebugListBoundaryIndicator(),
        ),
      ],
      itemBuilder: (context, index, item, anim) =>
          _Item(ctrl: ctrl, item: item, animation: anim),
    );
  }
}

class _DebugCenterIndicator extends StatelessWidget {
  const _DebugCenterIndicator({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return SizedBox(
      height: 4,
      child: Row(
        mainAxisSize: MainAxisSize.max,
        children: [
          Expanded(child: Container(color: Colors.red)),
          Expanded(child: Container(color: Colors.orange)),
          Expanded(child: Container(color: Colors.yellow)),
          Expanded(child: Container(color: Colors.green)),
          Expanded(child: Container(color: Colors.lightBlue)),
          Expanded(child: Container(color: Colors.blue)),
          Expanded(child: Container(color: Colors.purple)),
        ],
      ),
    );
  }
}

class _DebugListBoundaryIndicator extends StatelessWidget {
  const _DebugListBoundaryIndicator({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Container(height: 4, color: Colors.red);
  }
}

class _Item extends StatefulWidget {
  const _Item({
    Key? key,
    required this.ctrl,
    required this.item,
    required this.animation,
  }) : super(key: key);

  final TwoWayListController<int> ctrl;
  final int item;
  final Animation<double> animation;

  [@override](/user/override)
  State<_Item> createState() => _ItemState();
}

class _ItemState extends State<_Item> {
  var initialized = false;

  [@override](/user/override)
  void initState() {
    super.initState();
    Timer(const Duration(milliseconds: 300), () {
      if (mounted) {
        setState(() {
          initialized = true;
        });
      }
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    final itemIndex = widget.ctrl.items.indexOf(widget.item);
    final centerIndex = widget.ctrl.centerIndex;

    final rand = RandomColor(widget.item);
    late final Color color;
    if (itemIndex < 0) {
      color = Colors.grey[700]!;
    } else if (itemIndex < centerIndex) {
      color = rand.randomColor(
        colorHue: ColorHue.yellow,
        colorBrightness: ColorBrightness.veryLight,
        colorSaturation: ColorSaturation.mediumSaturation,
      );
    } else {
      color = rand.randomColor(
        colorHue: ColorHue.blue,
        colorBrightness: ColorBrightness.veryLight,
        colorSaturation: ColorSaturation.mediumSaturation,
      );
    }

    return SizeTransition(
      sizeFactor: widget.animation,
      axisAlignment: 0,
      child: Container(
        alignment: Alignment.center,
        color: color,
        height: 100 + widget.item % 4 * 60.0,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('Item: ${widget.item}'),
            if (!initialized)
              Container(
                width: 16,
                height: 16,
                margin: const EdgeInsets.only(left: 16, right: 16),
                child: const CircularProgressIndicator(color: Colors.black),
              ),
            if (initialized)
              IconButton(
                key: ValueKey('remove:${widget.item}'),
                icon: const Icon(Icons.delete),
                onPressed: () => widget.ctrl.remove(widget.item),
              )
          ],
        ),
      ),
    );
  }
}

更多关于Flutter双向滚动插件two_way_scrollable的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter双向滚动插件two_way_scrollable的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,我可以为你提供一个关于如何使用Flutter双向滚动插件two_way_scrollable的代码案例。这个插件允许你在Flutter应用中实现水平和垂直方向的双向滚动。

首先,你需要在你的pubspec.yaml文件中添加这个插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  two_way_scrollable: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来获取依赖。

接下来,下面是一个使用two_way_scrollable的简单示例代码:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Two Way Scrollable Example'),
        ),
        body: TwoWayScrollable(
          child: Container(
            width: 800, // 容器宽度
            height: 600, // 容器高度
            color: Colors.grey[200],
            child: GridView.builder(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 10, // 每行多少列
                crossAxisSpacing: 4.0,
                mainAxisSpacing: 4.0,
              ),
              itemCount: 100,
              itemBuilder: (BuildContext context, int index) {
                return Container(
                  color: Colors.primaries[index % Colors.primaries.length],
                  child: Center(
                    child: Text(
                      'Item $index',
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                );
              },
            ),
          ),
          scrollPhysics: const BouncingScrollPhysics(), // 可选,设置滚动物理特性
          horizontalScrollPhysics: const ClampingScrollPhysics(), // 可选,设置水平滚动物理特性
          verticalScrollPhysics: const BouncingScrollPhysics(), // 可选,设置垂直滚动物理特性
        ),
      ),
    );
  }
}

代码解释:

  1. 依赖添加:在pubspec.yaml中添加two_way_scrollable依赖。
  2. 导入包:在代码中导入two_way_scrollable包。
  3. 创建应用:在MyApp中,使用TwoWayScrollable包裹一个ContainerContainer中包含一个GridView,用于展示多个项目。
  4. 配置GridViewGridView.builder用于动态生成网格项目,这里我们设置了每行10个项目,并为每个项目分配了一个颜色。
  5. 滚动物理特性scrollPhysicshorizontalScrollPhysicsverticalScrollPhysics属性允许你自定义滚动的物理特性。

运行这段代码后,你将看到一个支持双向滚动的网格视图。你可以水平或垂直滚动查看所有项目。

请确保你使用的是最新版本的two_way_scrollable插件,并根据需要调整代码中的参数和样式。

回到顶部