进阶实战 Flutter for OpenHarmony:flutter_slidable 第三方库实战 - 列表滑动

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🎯 一、组件概述与应用场景

📋 1.1 flutter_slidable 简介

在移动应用开发中,列表项的侧滑操作是一种非常常见且直观的交互模式。flutter_slidable 是一个功能强大的 Flutter 插件,专门用于实现列表项的侧滑操作,如侧滑删除、侧滑标记、侧滑归档等。该插件完全使用 Dart 语言实现,不依赖任何原生平台代码,因此可以无缝运行在 OpenHarmony 平台上。

核心特性:

特性 说明
🎯 纯 Dart 实现 无需原生平台适配,跨平台一致性高
🎬 多种动画效果 支持 Scroll、Drawer、Behind、Stretch 等动画
↔️ 双向滑动 支持从左侧或右侧滑动显示操作菜单
🎨 自定义操作 支持自定义操作按钮、图标、颜色等
📡 滑动监听 支持监听滑动状态和滑动比例
🗑️ 滑动删除 支持滑动直接删除,带确认对话框
⚙️ 手势控制 支持控制滑动灵敏度、阈值等参数
🔔 通知回调 支持滑动开始、结束、取消等事件回调

💡 1.2 实际应用场景

邮件应用:侧滑删除、归档、标记已读/未读。

待办事项:侧滑完成、删除、编辑、设置提醒。

聊天列表:侧滑置顶、删除、静音、标记。

购物车:侧滑移除商品、收藏商品、修改数量。

文件管理:侧滑删除、重命名、移动、分享。

🏗️ 1.3 系统架构设计

复制代码
┌─────────────────────────────────────────────────────────┐
│                    UI 展示层                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │ 邮件列表页面 │  │ 待办列表页面 │  │ 购物车页面  │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    Slidable 组件层                       │
│  ┌─────────────────────────────────────────────────┐   │
│  │          Slidable / ActionPane / SlidableAction │   │
│  │  • startActionPane  • endActionPane             │   │
│  │  • ScrollMotion  • DrawerMotion  • BehindMotion │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    数据模型层                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │ TodoItem    │  │ MailItem    │  │ CartItem    │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
└─────────────────────────────────────────────────────────┘

📦 二、项目配置与依赖安装

🔧 2.1 添加依赖配置

打开项目根目录下的 pubspec.yaml 文件,添加以下配置:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter

  # flutter_slidable - 列表滑动操作组件
  flutter_slidable: ^3.1.1

配置说明:

  • flutter_slidable 是纯 Dart 实现的插件
  • 无需 OpenHarmony 平台适配,直接使用 pub.dev 版本即可
  • 支持所有 Flutter 平台,包括 OpenHarmony

📥 2.2 下载依赖

配置完成后,在项目根目录执行以下命令:

bash 复制代码
flutter pub get

🔐 2.3 权限配置

flutter_slidable 是纯 Dart 实现的 UI 组件,不需要配置任何平台权限。

📱 2.4 滑动动画类型

动画类型 说明
ScrollMotion 操作按钮跟随滑动滚动显示(默认)
DrawerMotion 操作按钮从边缘滑出,类似抽屉效果
BehindMotion 操作按钮固定在背后,滑动时逐渐显示
StretchMotion 操作按钮随滑动拉伸变形

🔧 三、核心功能详解

🎯 3.1 基本结构

dart 复制代码
import 'package:flutter_slidable/flutter_slidable.dart';

Slidable(
  key: ValueKey(index),
  startActionPane: ActionPane(
    motion: ScrollMotion(),
    children: [
      SlidableAction(
        onPressed: (context) {},
        backgroundColor: Colors.blue,
        icon: Icons.share,
        label: '分享',
      ),
    ],
  ),
  endActionPane: ActionPane(
    motion: ScrollMotion(),
    children: [
      SlidableAction(
        onPressed: (context) {},
        backgroundColor: Colors.red,
        icon: Icons.delete,
        label: '删除',
      ),
    ],
  ),
  child: ListTile(
    title: Text('列表项'),
  ),
)

📊 3.2 ActionPane 配置

dart 复制代码
ActionPane({
  required Widget motion,        // 滑动动画类型
  double? extentRatio,           // 操作区域占比(默认 0.25)
  List<Widget> children = const [], // 操作按钮列表
  DismissibleActionPane? dismissible, // 滑动删除配置
})

🔘 3.3 SlidableAction 配置

dart 复制代码
SlidableAction({
  required void Function(BuildContext)? onPressed, // 点击回调
  Color? backgroundColor,         // 背景颜色
  Color? foregroundColor,         // 前景颜色
  IconData? icon,                 // 图标
  String? label,                  // 文字标签
  bool autoClose = true,          // 点击后是否自动关闭
  double? spacing,                // 图标和文字间距
})

🗑️ 3.4 滑动删除

dart 复制代码
endActionPane: ActionPane(
  motion: ScrollMotion(),
  dismissible: DismissiblePane(
    onDismissed: () {
      _deleteItem(index);
    },
    confirmDismiss: () async {
      return await showDialog<bool>(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('确认删除'),
          actions: [
            TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('取消')),
            TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('删除')),
          ],
        ),
      ) ?? false;
    },
  ),
  children: [...],
)

📝 四、完整示例代码

下面是一个完整的列表滑动操作系统示例:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';

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

class SlidableApp extends StatelessWidget {
  const SlidableApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '列表滑动操作系统',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MainPage(),
    );
  }
}

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

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const TodoListPage(),
    const MailListPage(),
    const CartListPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(icon: Icon(Icons.checklist), label: '待办事项'),
          NavigationDestination(icon: Icon(Icons.mail), label: '邮件列表'),
          NavigationDestination(icon: Icon(Icons.shopping_cart), label: '购物车'),
        ],
      ),
    );
  }
}

// ============ 数据模型 ============

class TodoItem {
  final int id;
  final String title;
  final String subtitle;
  bool isCompleted;

  TodoItem({
    required this.id,
    required this.title,
    required this.subtitle,
    this.isCompleted = false,
  });
}

class MailItem {
  final int id;
  final String sender;
  final String subject;
  final String preview;
  final DateTime time;
  bool isRead;
  bool isStarred;

  MailItem({
    required this.id,
    required this.sender,
    required this.subject,
    required this.preview,
    required this.time,
    this.isRead = false,
    this.isStarred = false,
  });
}

class CartItem {
  final int id;
  final String name;
  final double price;
  int quantity;

  CartItem({
    required this.id,
    required this.name,
    required this.price,
    this.quantity = 1,
  });
}

// ============ 待办事项页面 ============

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

  @override
  State<TodoListPage> createState() => _TodoListPageState();
}

class _TodoListPageState extends State<TodoListPage> {
  final List<TodoItem> _items = List.generate(
    15,
    (index) => TodoItem(
      id: index,
      title: '待办事项 ${index + 1}',
      subtitle: '这是第 ${index + 1} 条待办事项的描述内容',
    ),
  );

  void _deleteItem(int index) {
    final item = _items[index];
    setState(() => _items.removeAt(index));
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('已删除: ${item.title}')),
    );
  }

  void _archiveItem(int index) {
    final item = _items[index];
    setState(() => _items.removeAt(index));
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('已归档: ${item.title}')),
    );
  }

  void _editItem(int index) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('编辑待办'),
        content: Text('编辑: ${_items[index].title}'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('保存')),
        ],
      ),
    );
  }

  void _toggleComplete(int index) {
    setState(() {
      _items[index].isCompleted = !_items[index].isCompleted;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('待办事项'),
        centerTitle: true,
        actions: [
          IconButton(icon: const Icon(Icons.add), onPressed: () {}),
        ],
      ),
      body: _items.isEmpty
          ? const Center(child: Text('暂无待办事项'))
          : ListView.builder(
              itemCount: _items.length,
              itemBuilder: (context, index) {
                final item = _items[index];
                return Slidable(
                  key: ValueKey(item.id),
                  startActionPane: ActionPane(
                    motion: const DrawerMotion(),
                    children: [
                      SlidableAction(
                        onPressed: (_) => _editItem(index),
                        backgroundColor: Colors.blue,
                        foregroundColor: Colors.white,
                        icon: Icons.edit,
                        label: '编辑',
                      ),
                      SlidableAction(
                        onPressed: (_) => _archiveItem(index),
                        backgroundColor: Colors.orange,
                        foregroundColor: Colors.white,
                        icon: Icons.archive,
                        label: '归档',
                      ),
                    ],
                  ),
                  endActionPane: ActionPane(
                    motion: const ScrollMotion(),
                    dismissible: DismissiblePane(
                      onDismissed: () => _deleteItem(index),
                    ),
                    children: [
                      SlidableAction(
                        onPressed: (_) => _toggleComplete(index),
                        backgroundColor: Colors.green,
                        foregroundColor: Colors.white,
                        icon: item.isCompleted ? Icons.undo : Icons.check,
                        label: item.isCompleted ? '撤销' : '完成',
                      ),
                      SlidableAction(
                        onPressed: (_) => _deleteItem(index),
                        backgroundColor: Colors.red,
                        foregroundColor: Colors.white,
                        icon: Icons.delete,
                        label: '删除',
                      ),
                    ],
                  ),
                  child: ListTile(
                    leading: Icon(
                      item.isCompleted ? Icons.check_circle : Icons.circle_outlined,
                      color: item.isCompleted ? Colors.green : Colors.grey,
                    ),
                    title: Text(
                      item.title,
                      style: TextStyle(
                        decoration: item.isCompleted ? TextDecoration.lineThrough : null,
                        color: item.isCompleted ? Colors.grey : null,
                      ),
                    ),
                    subtitle: Text(item.subtitle),
                    trailing: const Icon(Icons.chevron_right),
                  ),
                );
              },
            ),
    );
  }
}

// ============ 邮件列表页面 ============

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

  @override
  State<MailListPage> createState() => _MailListPageState();
}

class _MailListPageState extends State<MailListPage> {
  final List<MailItem> _items = List.generate(
    15,
    (index) => MailItem(
      id: index,
      sender: '发件人 ${index + 1}',
      subject: '邮件主题 ${index + 1}',
      preview: '这是邮件预览内容,显示邮件的简要信息...',
      time: DateTime.now().subtract(Duration(hours: index)),
      isRead: index > 3,
    ),
  );

  void _deleteMail(int index) {
    setState(() => _items.removeAt(index));
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已删除邮件')),
    );
  }

  void _toggleRead(int index) {
    setState(() {
      _items[index].isRead = !_items[index].isRead;
    });
  }

  void _toggleStar(int index) {
    setState(() {
      _items[index].isStarred = !_items[index].isStarred;
    });
  }

  void _archiveMail(int index) {
    setState(() => _items.removeAt(index));
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已归档邮件')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('邮件列表'),
        centerTitle: true,
      ),
      body: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          final item = _items[index];
          return Slidable(
            key: ValueKey(item.id),
            startActionPane: ActionPane(
              motion: const BehindMotion(),
              children: [
                SlidableAction(
                  onPressed: (_) => _archiveMail(index),
                  backgroundColor: Colors.blue,
                  foregroundColor: Colors.white,
                  icon: Icons.archive,
                  label: '归档',
                ),
                SlidableAction(
                  onPressed: (_) => _toggleStar(index),
                  backgroundColor: Colors.amber,
                  foregroundColor: Colors.white,
                  icon: item.isStarred ? Icons.star : Icons.star_border,
                  label: item.isStarred ? '取消星标' : '星标',
                ),
              ],
            ),
            endActionPane: ActionPane(
              motion: const StretchMotion(),
              children: [
                SlidableAction(
                  onPressed: (_) => _toggleRead(index),
                  backgroundColor: Colors.green,
                  foregroundColor: Colors.white,
                  icon: item.isRead ? Icons.mark_email_unread : Icons.mark_email_read,
                  label: item.isRead ? '未读' : '已读',
                ),
                SlidableAction(
                  onPressed: (_) => _deleteMail(index),
                  backgroundColor: Colors.red,
                  foregroundColor: Colors.white,
                  icon: Icons.delete,
                  label: '删除',
                ),
              ],
            ),
            child: Container(
              color: item.isRead ? null : Colors.blue.withOpacity(0.05),
              child: ListTile(
                leading: CircleAvatar(
                  child: Text(item.sender[0]),
                ),
                title: Row(
                  children: [
                    Expanded(
                      child: Text(
                        item.sender,
                        style: TextStyle(
                          fontWeight: item.isRead ? FontWeight.normal : FontWeight.bold,
                        ),
                      ),
                    ),
                    Text(
                      '${item.time.hour}:${item.time.minute.toString().padLeft(2, '0')}',
                      style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
                    ),
                  ],
                ),
                subtitle: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      item.subject,
                      style: TextStyle(
                        fontWeight: item.isRead ? FontWeight.normal : FontWeight.w500,
                      ),
                    ),
                    Text(item.preview, maxLines: 1, overflow: TextOverflow.ellipsis),
                  ],
                ),
                trailing: item.isStarred
                    ? const Icon(Icons.star, color: Colors.amber)
                    : null,
              ),
            ),
          );
        },
      ),
    );
  }
}

// ============ 购物车页面 ============

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

  @override
  State<CartListPage> createState() => _CartListPageState();
}

class _CartListPageState extends State<CartListPage> {
  final List<CartItem> _items = List.generate(
    8,
    (index) => CartItem(
      id: index,
      name: '商品 ${index + 1}',
      price: (index + 1) * 99.9,
      quantity: 1,
    ),
  );

  void _removeItem(int index) {
    setState(() => _items.removeAt(index));
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已移除商品')),
    );
  }

  void _addToFavorites(int index) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('已收藏: ${_items[index].name}')),
    );
  }

  void _changeQuantity(int index, int delta) {
    setState(() {
      _items[index].quantity = (_items[index].quantity + delta).clamp(1, 99);
    });
  }

  double get _totalPrice => _items.fold(0, (sum, item) => sum + item.price * item.quantity);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('购物车'),
        centerTitle: true,
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _items.length,
              itemBuilder: (context, index) {
                final item = _items[index];
                return Slidable(
                  key: ValueKey(item.id),
                  endActionPane: ActionPane(
                    motion: const DrawerMotion(),
                    children: [
                      SlidableAction(
                        onPressed: (_) => _addToFavorites(index),
                        backgroundColor: Colors.pink,
                        foregroundColor: Colors.white,
                        icon: Icons.favorite,
                        label: '收藏',
                      ),
                      SlidableAction(
                        onPressed: (_) => _removeItem(index),
                        backgroundColor: Colors.red,
                        foregroundColor: Colors.white,
                        icon: Icons.delete,
                        label: '删除',
                      ),
                    ],
                  ),
                  child: Card(
                    margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                    child: Padding(
                      padding: const EdgeInsets.all(12),
                      child: Row(
                        children: [
                          Container(
                            width: 60,
                            height: 60,
                            decoration: BoxDecoration(
                              color: Colors.grey.shade200,
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: const Icon(Icons.image, color: Colors.grey),
                          ),
                          const SizedBox(width: 12),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(item.name, style: const TextStyle(fontWeight: FontWeight.w500)),
                                Text('¥${item.price.toStringAsFixed(2)}', style: const TextStyle(color: Colors.red)),
                              ],
                            ),
                          ),
                          Row(
                            children: [
                              IconButton(
                                icon: const Icon(Icons.remove_circle_outline),
                                onPressed: item.quantity > 1 ? () => _changeQuantity(index, -1) : null,
                              ),
                              Text('${item.quantity}'),
                              IconButton(
                                icon: const Icon(Icons.add_circle_outline),
                                onPressed: () => _changeQuantity(index, 1),
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.white,
              boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)],
            ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('合计: ¥${_totalPrice.toStringAsFixed(2)}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                ElevatedButton(
                  onPressed: _items.isEmpty ? null : () {},
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red,
                    foregroundColor: Colors.white,
                  ),
                  child: const Text('结算'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

🏆 五、最佳实践与注意事项

⚠️ 5.1 使用唯一 Key

每个 Slidable 必须有唯一的 key,否则会导致状态混乱:

dart 复制代码
Slidable(
  key: ValueKey(item.id),  // 使用唯一标识
  ...
)

🔄 5.2 自动关闭控制

默认情况下,点击操作按钮后会自动关闭滑动面板。可以通过 autoClose 参数控制:

dart 复制代码
SlidableAction(
  autoClose: false,  // 点击后不自动关闭
  ...
)

📐 5.3 操作区域比例

通过 extentRatio 控制操作区域占列表项宽度的比例:

dart 复制代码
ActionPane(
  extentRatio: 0.3,  // 操作区域占 30%
  ...
)

🎨 5.4 自定义操作按钮

除了 SlidableAction,也可以使用自定义 Widget:

dart 复制代码
ActionPane(
  children: [
    Expanded(
      child: Container(
        color: Colors.purple,
        child: const Center(
          child: Text('自定义按钮'),
        ),
      ),
    ),
  ],
)

📱 5.5 常见问题处理

滑动冲突 :如果列表中有多个 Slidable,建议使用 SlidableAutoCloseBehavior 包裹列表,确保同时只有一个 Slidable 处于打开状态。

dart 复制代码
SlidableAutoCloseBehavior(
  child: ListView.builder(...),
)

📌 六、总结

本文通过一个完整的列表滑动操作系统案例,深入讲解了 flutter_slidable 插件的使用方法与最佳实践:

基础用法:掌握 Slidable、ActionPane、SlidableAction 的基本配置。

滑动动画:学会使用 ScrollMotion、DrawerMotion、BehindMotion、StretchMotion。

滑动删除:实现带确认对话框的滑动删除功能。

实际应用:了解待办事项、邮件列表、购物车等场景的应用。

掌握这些技巧,你就能构建出流畅、直观的列表交互体验,提升应用的用户体验。


参考资料

相关推荐
啥都想学点3 小时前
第1天:搭建 flutter 和 Android 环境
android·flutter
蓝帆傲亦3 小时前
Vue.js 大数据处理全景解析:从加载策略到渲染优化的完全手册
前端·vue.js·flutter
九狼3 小时前
Flutter Riverpod + MVI 状态管理实现的提示词优化器
前端·flutter·github
阿林来了4 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 总结与未来展望
flutter
早點睡39013 小时前
进阶实战 Flutter for OpenHarmony:CustomPainter 组件实战 - 自定义绘图与画板
flutter
早點睡39014 小时前
进阶实战 Flutter for OpenHarmony:video_player 第三方库实战
flutter
键盘鼓手苏苏17 小时前
Flutter for OpenHarmony:csslib 强力 CSS 样式解析器,构建自定义渲染引擎的基石(Dart 官方解析库) 深度解析与鸿蒙适配指南
css·flutter·harmonyos
lili-felicity18 小时前
进阶实战 Flutter for OpenHarmony:palette_generator 第三方库实战 - 图片配色提取
flutter
lili-felicity21 小时前
进阶实战 Flutter for OpenHarmony:syncfusion_flutter_charts 第三方库实战 - 企业级图表系统
flutter