前言

Flutter是Google开发的开源UI工具包,支持用一套代码构建iOSAndroidWebWindowsmacOSLinux六大平台应用,实现"一次编写,多处运行"。

OpenHarmony是由开放原子开源基金会运营的分布式操作系统,为全场景智能设备提供统一底座,具有多设备支持、模块化设计、分布式能力和开源开放等特性。

Flutter for OpenHarmony技术方案使开发者能够:

  1. 复用Flutter现有代码(Skia渲染引擎、热重载、丰富组件库)
  2. 快速构建符合OpenHarmony规范的UI
  3. 降低多端开发成本
  4. 利用Dart生态插件资源加速生态建设

本文详细解析了一个完整的 Flutter 左右滑动操作应用的开发过程。这个应用展示了如何实现两种常见的滑动交互:左滑显示操作菜单和右滑删除,这两种交互在现代移动应用中非常常见。包含手势识别、动画控制、操作菜单、删除确认对话框、撤销功能等核心特性。使用 RepaintBoundary 优化性能,交互流畅自然。

先看效果

Flutte实现的 web端实时预览 完整效果

在这里插入图片描述

在鸿蒙 真机模拟器上成功运行后的效果

在这里插入图片描述


📋 目录

项目结构说明

应用入口

演示页面 (SwipeListPage)

SwipeableListItem 组件

SwipeActionMenu 组件

DismissibleListItem 组件

数据模型 (SwipeItemModel)


📁 项目结构说明

文件目录结构

lib/
├── main.dart                           # 应用入口文件
├── models/                             # 数据模型目录
│   └── swipe_item_model.dart          # 滑动项数据模型
├── pages/                              # 页面目录
│   └── swipe_list_page.dart           # 滑动操作列表页面
└── widgets/                            # 组件目录
    ├── swipeable_list_item.dart       # 左滑菜单列表项组件
    ├── swipe_action_menu.dart          # 左滑操作菜单组件
    └── dismissible_list_item.dart     # 右滑删除列表项组件

文件说明

入口文件

lib/main.dart

  • 应用入口点,包含 main() 函数
  • 定义 MyApp 类,配置应用主题
  • 设置应用标题为"滑动操作演示"
页面文件

lib/pages/swipe_list_page.dart

  • SwipeListPage 类:滑动操作列表页面主类
    • 使用 TabController 管理两个标签页
    • 管理左滑菜单和右滑删除两个列表
    • 实现删除和撤销功能
组件文件

lib/widgets/swipeable_list_item.dart

  • SwipeableListItem 组件:左滑菜单列表项组件
    • 实现左滑显示操作菜单
    • 手势识别和动画控制
    • 支持编辑、分享、删除操作

lib/widgets/swipe_action_menu.dart

  • SwipeAction 类:操作菜单项数据模型
  • SwipeActionMenu 组件:左滑操作菜单组件
    • 显示多个操作按钮
    • 支持自定义颜色和图标

lib/widgets/dismissible_list_item.dart

  • DismissibleListItem 组件:右滑删除列表项组件
    • 使用 Dismissible 实现滑动删除
    • 显示删除确认对话框
    • 显示删除背景提示
数据模型

lib/models/swipe_item_model.dart

  • SwipeItem 类:滑动项数据模型
    • 包含商品信息(标题、副标题、分类、价格、状态等)
  • SwipeItemGenerator 类:数据生成器
    • 生成演示用的商品数据

组件依赖关系

main.dart
  └── pages/swipe_list_page.dart        (导入演示页面)
      ├── models/swipe_item_model.dart  (导入数据模型)
      ├── widgets/swipeable_list_item.dart  (导入左滑菜单组件)
      └── widgets/dismissible_list_item.dart  (导入右滑删除组件)
          └── models/swipe_item_model.dart     (导入数据模型)
              └── widgets/swipe_action_menu.dart  (导入操作菜单组件)

数据流向

  1. 数据生成SwipeItemGenerator.generateItems() 生成商品数据
  2. 数据加载SwipeListPage 加载数据到两个列表
  3. 列表渲染ListView.builder 遍历数据,创建滑动列表项
  4. 手势识别:用户滑动时触发手势处理
  5. 操作反馈:点击操作按钮或确认删除后执行相应操作
  6. 撤销功能:删除后显示 SnackBar,支持撤销操作

应用入口

1. main() 函数

import 'package:flutter/material.dart';
import 'pages/swipe_list_page.dart';

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

应用入口,导入滑动列表页面。


2. MyApp 类 - 主题配置

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '滑动操作演示',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,  // 蓝色主题
          brightness: Brightness.light,  // 浅色模式
        ),
        useMaterial3: true,
      ),
      home: const SwipeListPage(),
    );
  }
}

配置浅色主题,使用蓝色作为种子颜色。


演示页面 (SwipeListPage)

1. 类定义和状态管理

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

  
  State<SwipeListPage> createState() => _SwipeListPageState();
}

class _SwipeListPageState extends State<SwipeListPage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;  // 标签控制器
  List<SwipeItem> _swipeableItems = [];  // 左滑菜单列表
  List<SwipeItem> _dismissibleItems = [];  // 右滑删除列表
  SwipeItem? _lastDeletedItem;  // 最后删除的项(用于撤销)
  int? _lastDeletedIndex;  // 最后删除的索引

SingleTickerProviderStateMixin 提供 TabController 所需的 vsync。维护两个列表和删除信息,用于撤销功能。


2. 数据加载

void _loadData() {
  setState(() {
    _swipeableItems = SwipeItemGenerator.generateItems(20);
    _dismissibleItems = SwipeItemGenerator.generateItems(20);
  });
}


void initState() {
  super.initState();
  _tabController = TabController(length: 2, vsync: this);
  _loadData();  // 初始化时加载数据
}


void dispose() {
  _tabController.dispose();
  super.dispose();
}

_loadData() 生成并加载数据,initState() 中初始化 TabController 并加载数据。


3. 删除操作

void _deleteSwipeableItem(int index) {
  setState(() {
    _lastDeletedItem = _swipeableItems[index];
    _lastDeletedIndex = index;
    _swipeableItems.removeAt(index);  // 从列表移除
  });
  
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: const Text('已删除'),
      duration: const Duration(seconds: 2),
      behavior: SnackBarBehavior.floating,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
      ),
      action: SnackBarAction(
        label: '撤销',
        textColor: Colors.white,
        onPressed: () {
          if (_lastDeletedItem != null && _lastDeletedIndex != null) {
            setState(() {
              _swipeableItems.insert(_lastDeletedIndex!, _lastDeletedItem!);  // 恢复
              _lastDeletedItem = null;
              _lastDeletedIndex = null;
            });
          }
        },
      ),
    ),
  );
}

void _deleteDismissibleItem(String id) {
  final index = _dismissibleItems.indexWhere((item) => item.id == id);
  if (index != -1) {
    setState(() {
      _lastDeletedItem = _dismissibleItems[index];
      _lastDeletedIndex = index;
      _dismissibleItems.removeAt(index);
    });
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: const Text('已删除'),
        duration: const Duration(seconds: 2),
        behavior: SnackBarBehavior.floating,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
        action: SnackBarAction(
          label: '撤销',
          textColor: Colors.white,
          onPressed: () {
            if (_lastDeletedItem != null && _lastDeletedIndex != null) {
              setState(() {
                _dismissibleItems.insert(_lastDeletedIndex!, _lastDeletedItem!);
                _lastDeletedItem = null;
                _lastDeletedIndex = null;
              });
            }
          },
        ),
      ),
    );
  }
}

删除操作保存被删除的项和索引,显示 SnackBar 提示,支持撤销恢复。


4. 页面布局结构


Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.grey.shade50,
    appBar: AppBar(
      title: const Text(
        '滑动操作演示',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          letterSpacing: -0.5,
        ),
      ),
      centerTitle: true,
      elevation: 0,
      backgroundColor: Theme.of(context).colorScheme.primary,
      foregroundColor: Colors.white,
      bottom: TabBar(
        controller: _tabController,
        indicatorColor: Colors.white,
        indicatorWeight: 3,
        indicatorSize: TabBarIndicatorSize.tab,
        labelColor: Colors.white,
        unselectedLabelColor: Colors.white70,
        labelStyle: const TextStyle(
          fontWeight: FontWeight.w600,
          fontSize: 14,
        ),
        tabs: const [
          Tab(
            icon: Icon(Icons.swipe_left, size: 20),
            text: '左滑菜单',
          ),
          Tab(
            icon: Icon(Icons.delete_outline, size: 20),
            text: '滑动删除',
          ),
        ],
      ),
      actions: [
        IconButton(
          icon: const Icon(Icons.refresh),
          tooltip: '刷新',
          onPressed: _loadData,
        ),
      ],
    ),
    body: TabBarView(
      controller: _tabController,
      children: [
        _buildSwipeableList(),  // 左滑菜单列表
        _buildDismissibleList(),  // 右滑删除列表
      ],
    ),
  );
}

页面使用 TabBarTabBarView 实现两个标签页,分别显示左滑菜单和右滑删除列表。


5. 列表构建

Widget _buildSwipeableList() {
  if (_swipeableItems.isEmpty) {
    return _buildEmptyState('左滑显示操作菜单', Icons.swipe_left);
  }

  return ListView.builder(
    padding: const EdgeInsets.symmetric(vertical: 8),
    itemCount: _swipeableItems.length,
    cacheExtent: 500,  // 预加载范围
    itemBuilder: (context, index) {
      return SwipeableListItem(
        key: ValueKey(_swipeableItems[index].id),
        item: _swipeableItems[index],
        index: index,
        onDelete: () => _deleteSwipeableItem(index),
        onEdit: () {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('编辑: ${_swipeableItems[index].title}'),
              duration: const Duration(milliseconds: 1500),
              behavior: SnackBarBehavior.floating,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10),
              ),
            ),
          );
        },
        onShare: () {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('分享: ${_swipeableItems[index].title}'),
              duration: const Duration(milliseconds: 1500),
              behavior: SnackBarBehavior.floating,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10),
              ),
            ),
          );
        },
      );
    },
  );
}

Widget _buildDismissibleList() {
  if (_dismissibleItems.isEmpty) {
    return _buildEmptyState('向右滑动删除', Icons.swipe);
  }

  return ListView.builder(
    padding: const EdgeInsets.symmetric(vertical: 8),
    itemCount: _dismissibleItems.length,
    cacheExtent: 500,
    itemBuilder: (context, index) {
      return DismissibleListItem(
        item: _dismissibleItems[index],
        index: index,
        onDismissed: () => _deleteDismissibleItem(_dismissibleItems[index].id),
      );
    },
  );
}

Widget _buildEmptyState(String message, IconData icon) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(
          padding: const EdgeInsets.all(24),
          decoration: BoxDecoration(
            color: Colors.white,
            shape: BoxShape.circle,
            boxShadow: [
              BoxShadow(
                color: Colors.black.withValues(alpha: 0.05),
                blurRadius: 20,
              ),
            ],
          ),
          child: Icon(
            icon,
            size: 64,
            color: Colors.grey.shade400,
          ),
        ),
        const SizedBox(height: 24),
        Text(
          '暂无数据',
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.w600,
            color: Colors.grey.shade700,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          message,
          style: TextStyle(
            fontSize: 14,
            color: Colors.grey.shade500,
          ),
        ),
        const SizedBox(height: 32),
        ElevatedButton.icon(
          onPressed: _loadData,
          icon: const Icon(Icons.refresh),
          label: const Text('重新加载'),
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12),
            ),
          ),
        ),
      ],
    ),
  );
}

两个列表使用 ListView.builder 构建,列表为空时显示空状态提示。使用 ValueKey 优化性能。


SwipeableListItem 组件

1. 类定义和动画

class SwipeableListItem extends StatefulWidget {
  final SwipeItem item;
  final int index;
  final VoidCallback? onDelete;
  final VoidCallback? onEdit;
  final VoidCallback? onShare;

  const SwipeableListItem({
    super.key,
    required this.item,
    required this.index,
    this.onDelete,
    this.onEdit,
    this.onShare,
  });

  
  State<SwipeableListItem> createState() => _SwipeableListItemState();
}

class _SwipeableListItemState extends State<SwipeableListItem>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _slideAnimation;  // 滑动动画
  double _dragOffset = 0.0;  // 拖拽偏移量
  bool _isDragging = false;  // 是否正在拖拽
  static const double _actionMenuWidth = 240.0;  // 操作菜单宽度

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 250),  // 250ms 动画
    );
    _slideAnimation = Tween<double>(
      begin: 0.0,
      end: -_actionMenuWidth,  // 向左滑动,负值
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOut,
    ));
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

SingleTickerProviderStateMixin 提供动画控制器所需的 vsync_slideAnimation 控制内容向左滑动,最大偏移为操作菜单宽度。


2. 手势处理

void _onPanStart(DragStartDetails details) {
  setState(() {
    _isDragging = true;  // 开始拖拽
  });
}

void _onPanUpdate(DragUpdateDetails details) {
  setState(() {
    _dragOffset += details.delta.dx;  // 累加水平偏移
    // 限制滑动范围:不能超过菜单宽度,不能向右滑动
    _dragOffset = math.max(-_actionMenuWidth, math.min(0, _dragOffset));
  });
}

void _onPanEnd(DragEndDetails details) {
  setState(() {
    _isDragging = false;  // 结束拖拽
  });

  final threshold = _actionMenuWidth * 0.4;  // 40% 阈值
  final velocity = details.velocity.pixelsPerSecond.dx;  // 滑动速度

  if (_dragOffset.abs() > threshold || velocity < -500) {
    // 超过阈值或快速滑动:展开菜单
    _controller.forward();
    _dragOffset = -_actionMenuWidth;
  } else {
    // 未超过阈值:收起菜单
    _controller.reverse();
    _dragOffset = 0;
  }
}

手势处理:_onPanStart 标记开始拖拽,_onPanUpdate 更新偏移并限制范围,_onPanEnd 根据阈值和速度决定展开或收起。


3. 菜单控制

void _closeMenu() {
  if (_controller.isCompleted || _dragOffset < 0) {
    _controller.reverse();  // 收起菜单
    setState(() {
      _dragOffset = 0;
    });
  }
}

_closeMenu() 收起菜单,在点击内容或操作按钮后调用。


4. 内容构建


Widget build(BuildContext context) {
  return GestureDetector(
    onTap: _closeMenu,  // 点击收起菜单
    onHorizontalDragStart: _onPanStart,
    onHorizontalDragUpdate: _onPanUpdate,
    onHorizontalDragEnd: _onPanEnd,
    child: Stack(
      clipBehavior: Clip.none,
      children: [
        // 背景操作菜单
        Positioned.fill(
          child: Align(
            alignment: Alignment.centerRight,
            child: SwipeActionMenu(
              actions: [
                SwipeAction(
                  label: '编辑',
                  icon: Icons.edit_outlined,
                  color: Colors.blue.shade400,
                  onTap: () {
                    _closeMenu();
                    widget.onEdit?.call();
                  },
                ),
                SwipeAction(
                  label: '分享',
                  icon: Icons.share_outlined,
                  color: Colors.green.shade400,
                  onTap: () {
                    _closeMenu();
                    widget.onShare?.call();
                  },
                ),
                SwipeAction(
                  label: '删除',
                  icon: Icons.delete_outline,
                  color: Colors.red.shade400,
                  onTap: () {
                    _closeMenu();
                    widget.onDelete?.call();
                  },
                ),
              ],
            ),
          ),
        ),
        // 内容卡片
        AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            final offset = _isDragging ? _dragOffset : _slideAnimation.value;
            return Transform.translate(
              offset: Offset(offset, 0),  // 水平平移
              child: _buildContent(),
            );
          },
        ),
      ],
    ),
  );
}

使用 Stack 布局,背景是操作菜单,前景是内容卡片。GestureDetector 处理手势,AnimatedBuilder 实现滑动动画。拖拽时使用 _dragOffset,动画时使用 _slideAnimation.value


5. 内容卡片构建

Widget _buildContent() {
  return RepaintBoundary(
    child: Container(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.06),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: () {
            _closeMenu();
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('点击了: ${widget.item.title}'),
                duration: const Duration(milliseconds: 1500),
                behavior: SnackBarBehavior.floating,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            );
          },
          borderRadius: BorderRadius.circular(16),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                _buildAvatar(),  // 头像
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Expanded(
                            child: Text(
                              widget.item.title,
                              style: const TextStyle(
                                fontSize: 16,
                                fontWeight: FontWeight.w600,
                                letterSpacing: -0.3,
                              ),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                          const SizedBox(width: 8),
                          _buildStatusChip(),  // 状态标签
                        ],
                      ),
                      const SizedBox(height: 6),
                      Text(
                        widget.item.subtitle,
                        style: TextStyle(
                          fontSize: 13,
                          color: Colors.grey.shade600,
                          letterSpacing: -0.2,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                      const SizedBox(height: 10),
                      Row(
                        children: [
                          _buildCategoryTag(),  // 分类标签
                          const Spacer(),
                          Text(
                            ${widget.item.price.toStringAsFixed(2)}',
                            style: TextStyle(
                              fontSize: 18,
                              fontWeight: FontWeight.bold,
                              color: Theme.of(context).colorScheme.primary,
                              letterSpacing: -0.5,
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 8),
                Icon(
                  Icons.chevron_left,
                  color: Colors.grey.shade300,
                  size: 24,
                ),
              ],
            ),
          ),
        ),
      ),
    ),
  );
}

内容卡片使用 RepaintBoundary 优化性能,包含头像、标题、副标题、状态标签、分类标签和价格。


SwipeActionMenu 组件

1. SwipeAction 类

class SwipeAction {
  final String label;  // 标签文字
  final IconData icon;  // 图标
  final Color color;  // 颜色
  final VoidCallback onTap;  // 点击回调

  const SwipeAction({
    required this.label,
    required this.icon,
    required this.color,
    required this.onTap,
  });
}

操作菜单项数据模型,包含标签、图标、颜色和回调。


2. SwipeActionMenu 组件

class SwipeActionMenu extends StatelessWidget {
  final List<SwipeAction> actions;
  final double width;

  const SwipeActionMenu({
    super.key,
    required this.actions,
    this.width = 80,  // 每个按钮宽度
  });

  
  Widget build(BuildContext context) {
    return Container(
      width: width * actions.length,  // 总宽度 = 按钮数 × 按钮宽度
      decoration: BoxDecoration(
        color: Colors.grey.shade900,  // 深色背景
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: actions.asMap().entries.map((entry) {
          final index = entry.key;
          final action = entry.value;
          return _buildActionButton(action, index);
        }).toList(),
      ),
    );
  }

  Widget _buildActionButton(SwipeAction action, int index) {
    return Expanded(
      child: Container(
        decoration: BoxDecoration(
          border: Border(
            left: index > 0
                ? BorderSide(color: Colors.grey.shade800, width: 0.5)
                : BorderSide.none,  // 第一个按钮无左边框
          ),
        ),
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: action.onTap,
            child: Container(
              padding: const EdgeInsets.symmetric(vertical: 20),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Container(
                    width: 44,
                    height: 44,
                    decoration: BoxDecoration(
                      color: action.color.withValues(alpha: 0.15),  // 半透明背景
                      shape: BoxShape.circle,
                    ),
                    child: Icon(
                      action.icon,
                      color: action.color,
                      size: 22,
                    ),
                  ),
                  const SizedBox(height: 6),
                  Text(
                    action.label,
                    style: TextStyle(
                      color: action.color,
                      fontSize: 11,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

操作菜单使用 Row 水平排列按钮,每个按钮包含圆形图标和文字标签。使用 InkWell 提供点击反馈。


DismissibleListItem 组件

1. 类定义和布局

class DismissibleListItem extends StatelessWidget {
  final SwipeItem item;
  final int index;
  final VoidCallback onDismissed;
  final VoidCallback? onTap;

  const DismissibleListItem({
    super.key,
    required this.item,
    required this.index,
    required this.onDismissed,
    this.onTap,
  });

  
  Widget build(BuildContext context) {
    return Dismissible(
      key: ValueKey(item.id),
      direction: DismissDirection.endToStart,  // 只能从右向左滑动
      background: _buildDismissBackground(),  // 删除背景
      secondaryBackground: _buildDismissBackground(),
      confirmDismiss: (direction) async {
        return await _showDeleteConfirmDialog(context);  // 显示确认对话框
      },
      onDismissed: (direction) {
        onDismissed();  // 确认后执行删除
      },
      child: _buildContent(context),
    );
  }
}

使用 Dismissible 实现滑动删除。direction: endToStart 限制只能从右向左滑动。confirmDismiss 显示确认对话框。


2. 删除确认对话框

Future<bool?> _showDeleteConfirmDialog(BuildContext context) {
  return showDialog<bool>(
    context: context,
    barrierDismissible: true,
    builder: (context) => AlertDialog(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
      ),
      title: Row(
        children: [
          Icon(
            Icons.warning_amber_rounded,
            color: Colors.orange.shade400,
            size: 28,
          ),
          const SizedBox(width: 12),
          const Text(
            '确认删除',
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 20,
            ),
          ),
        ],
      ),
      content: Text(
        '确定要删除 "${item.title}" 吗?',
        style: TextStyle(fontSize: 15, color: Colors.grey.shade700),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(false),  // 取消
          style: TextButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
          ),
          child: Text(
            '取消',
            style: TextStyle(color: Colors.grey.shade600),
          ),
        ),
        ElevatedButton(
          onPressed: () => Navigator.of(context).pop(true),  // 确认
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.red.shade400,
            foregroundColor: Colors.white,
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10),
            ),
          ),
          child: const Text('删除'),
        ),
      ],
    ),
  );
}

删除确认对话框使用 AlertDialog,包含警告图标、提示文字和取消/确认按钮。返回 true 表示确认删除。


3. 删除背景

Widget _buildDismissBackground() {
  return Container(
    margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [
          Colors.red.shade400,
          Colors.red.shade500,
        ],
        begin: Alignment.centerLeft,
        end: Alignment.centerRight,
      ),
      borderRadius: BorderRadius.circular(16),
    ),
    alignment: Alignment.centerRight,
    padding: const EdgeInsets.only(right: 24),
    child: const Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(
          Icons.delete_outline,
          color: Colors.white,
          size: 32,
        ),
        SizedBox(height: 8),
        Text(
          '删除',
          style: TextStyle(
            color: Colors.white,
            fontSize: 14,
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    ),
  );
}

删除背景使用红色渐变,显示删除图标和文字,提示用户滑动删除操作。


4. 内容构建

Widget _buildContent(BuildContext context) {
  return RepaintBoundary(
    child: Container(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.06),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: () {
            onTap?.call();
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('点击了: ${item.title}'),
                duration: const Duration(milliseconds: 1500),
                behavior: SnackBarBehavior.floating,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            );
          },
          borderRadius: BorderRadius.circular(16),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                _buildAvatar(context),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Expanded(
                            child: Text(
                              item.title,
                              style: const TextStyle(
                                fontSize: 16,
                                fontWeight: FontWeight.w600,
                                letterSpacing: -0.3,
                              ),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                          const SizedBox(width: 8),
                          _buildStatusChip(),
                        ],
                      ),
                      const SizedBox(height: 6),
                      Text(
                        item.subtitle,
                        style: TextStyle(
                          fontSize: 13,
                          color: Colors.grey.shade600,
                          letterSpacing: -0.2,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                      const SizedBox(height: 10),
                      Row(
                        children: [
                          _buildCategoryTag(context),
                          const Spacer(),
                          Text(
                            ${item.price.toStringAsFixed(2)}',
                            style: TextStyle(
                              fontSize: 18,
                              fontWeight: FontWeight.bold,
                              color: Theme.of(context).colorScheme.primary,
                              letterSpacing: -0.5,
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 8),
                Icon(
                  Icons.swipe_left,  // 提示可以滑动
                  color: Colors.grey.shade300,
                  size: 24,
                ),
              ],
            ),
          ),
        ),
      ),
    ),
  );
}

内容卡片与左滑菜单类似,但右侧显示滑动图标提示。使用 RepaintBoundary 优化性能。


数据模型 (SwipeItemModel)

1. SwipeItem 类

class SwipeItem {
  final String id;  // 唯一标识
  final String title;  // 标题
  final String subtitle;  // 副标题
  final String category;  // 分类
  final double price;  // 价格
  final String status;  // 状态
  final String avatar;  // 头像(emoji)
  final DateTime createTime;  // 创建时间

  SwipeItem({
    required this.id,
    required this.title,
    required this.subtitle,
    required this.category,
    required this.price,
    required this.status,
    required this.avatar,
    required this.createTime,
  });

  SwipeItem copyWith({
    String? id,
    String? title,
    String? subtitle,
    String? category,
    double? price,
    String? status,
    String? avatar,
    DateTime? createTime,
  }) {
    return SwipeItem(
      id: id ?? this.id,
      title: title ?? this.title,
      subtitle: subtitle ?? this.subtitle,
      category: category ?? this.category,
      price: price ?? this.price,
      status: status ?? this.status,
      avatar: avatar ?? this.avatar,
      createTime: createTime ?? this.createTime,
    );
  }
}

滑动项数据模型,包含商品信息。copyWith 方法用于创建副本并修改部分属性。


2. SwipeItemGenerator 类

class SwipeItemGenerator {
  static List<SwipeItem> generateItems(int count, {int startId = 0}) {
    final categories = ['电子产品', '服装', '食品', '图书', '家居', '运动', '美妆', '数码'];
    final titles = [
      'iPhone 15 Pro Max',
      'MacBook Pro M3',
      'AirPods Pro',
      'iPad Air',
      'Apple Watch',
      'Nike运动鞋',
      'Adidas外套',
      'Uniqlo T恤',
      '星巴克咖啡',
      '肯德基套餐',
    ];
    final subtitles = [
      '最新款旗舰手机',
      '强大的M3芯片',
      '主动降噪耳机',
      '轻薄便携平板',
      '健康监测手表',
      '舒适透气',
      '时尚潮流',
      '简约百搭',
      '香浓醇厚',
      '美味可口',
    ];
    final statuses = ['在售', '热销', '新品', '限时', '预售'];

    return List.generate(count, (index) {
      final id = startId + index;
      return SwipeItem(
        id: 'item_$id',
        title: titles[id % titles.length],
        subtitle: subtitles[id % subtitles.length],
        category: categories[id % categories.length],
        price: 99.99 + (id * 10.5),
        status: statuses[id % statuses.length],
        avatar: String.fromCharCode(65 + (id % 26)),  // A-Z
        createTime: DateTime.now().subtract(Duration(days: id % 30)),
      );
    });
  }
}

数据生成器创建演示数据。生成指定数量的商品,使用模运算循环使用标题、分类等数据。


使用示例

在页面中使用滑动操作

class MyPage extends StatefulWidget {
  
  State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  List<SwipeItem> _items = [];

  
  void initState() {
    super.initState();
    _items = SwipeItemGenerator.generateItems(10);
  }

  void _deleteItem(int index) {
    setState(() {
      _items.removeAt(index);
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('滑动操作')),
      body: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          // 左滑菜单
          return SwipeableListItem(
            key: ValueKey(_items[index].id),
            item: _items[index],
            index: index,
            onDelete: () => _deleteItem(index),
            onEdit: () {
              print('编辑: ${_items[index].title}');
            },
            onShare: () {
              print('分享: ${_items[index].title}');
            },
          );
          
          // 或右滑删除
          // return DismissibleListItem(
          //   item: _items[index],
          //   index: index,
          //   onDismissed: () => _deleteItem(index),
          // );
        },
      ),
    );
  }
}

使用步骤:

  1. 准备数据(使用 SwipeItemGenerator 或自定义数据)
  2. 使用 ListView.builder 遍历数据
  3. 为每个项创建 SwipeableListItem(左滑菜单)或 DismissibleListItem(右滑删除)
  4. 实现相应的回调函数(删除、编辑、分享等)

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐