Flutter双向滚动插件two_way_scrollable的使用
Flutter双向滚动插件two_way_scrollable的使用
插件简介
two_way_scrollable
是一个Flutter插件,提供了一组可以在两个方向上扩展的可滚动组件。这些组件可以正确地填充视口,即使内容不足。它特别适用于需要在顶部和底部同时添加或删除项目的场景。
主要特性
- TwoWayCustomScrollView:这是一个
CustomScrollView
的替代品,支持在两个方向上扩展。 - TwoWayListView:这是一个基于
TwoWayCustomScrollView
和SliverTwoWayList
的AnimatedListView
类似物,支持在两个方向上扩展。 - SliverTwoWayList:一组
Sliver
组件,可以与TwoWayCustomScrollView
或CustomScrollView
一起使用,以实现列表中的任意定位。
注意事项
TwoWayCustomScrollView
和TwoWayListView
只允许锚定到顶部或底部,但不能锚定到中间。
安装
在 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
更多关于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(), // 可选,设置垂直滚动物理特性
),
),
);
}
}
代码解释:
- 依赖添加:在
pubspec.yaml
中添加two_way_scrollable
依赖。 - 导入包:在代码中导入
two_way_scrollable
包。 - 创建应用:在
MyApp
中,使用TwoWayScrollable
包裹一个Container
,Container
中包含一个GridView
,用于展示多个项目。 - 配置GridView:
GridView.builder
用于动态生成网格项目,这里我们设置了每行10个项目,并为每个项目分配了一个颜色。 - 滚动物理特性:
scrollPhysics
、horizontalScrollPhysics
和verticalScrollPhysics
属性允许你自定义滚动的物理特性。
运行这段代码后,你将看到一个支持双向滚动的网格视图。你可以水平或垂直滚动查看所有项目。
请确保你使用的是最新版本的two_way_scrollable
插件,并根据需要调整代码中的参数和样式。