在这里插入图片描述

Card组件不仅能展示静态内容,还可以通过结合各种交互组件实现丰富的用户交互体验。本文将详细介绍Card组件的交互设计模式、手势识别、点击反馈、状态切换等交互特性。


一、Card交互模式

Card组件支持多种交互模式,可以根据应用场景选择合适的交互方式。

1.1 交互模式分类

Card交互

点击交互

滑动交互

拖拽交互

长按交互

整卡点击

按钮点击

图标点击

横向滑动

纵向滑动

双向滑动

重新排序

移动位置

拖入文件夹

上下文菜单

多选模式

快捷操作

1.2 交互场景对比

交互类型 适用场景 实现难度 用户体验 推荐指数
点击卡片 进入详情页 简单 直观 ⭐⭐⭐⭐⭐
点击按钮 执行操作 简单 明确 ⭐⭐⭐⭐⭐
滑动删除 快捷操作 中等 高效 ⭐⭐⭐⭐
拖拽排序 个性化布局 较难 有趣 ⭐⭐⭐
长按菜单 更多选项 中等 便捷 ⭐⭐⭐⭐

二、点击交互

2.1 整个Card可点击

最简单的交互方式是让整个Card响应点击:

class ClickableCard extends StatelessWidget {
  final String title;
  final String subtitle;
  final VoidCallback onTap;

  const ClickableCard({
    super.key,
    required this.title,
    required this.subtitle,
    required this.onTap,
  });

  
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
        onTap: onTap,
        splashColor: Colors.blue.withOpacity(0.3),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                title,
                style: const TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 8),
              Text(
                subtitle,
                style: TextStyle(
                  color: Colors.grey.shade600,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

2.2 Card内部按钮交互

在Card内部添加操作按钮:

Card(
  child: Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '操作卡片',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 12),
        const Text('这是一个带有操作按钮的卡片'),
        const SizedBox(height: 16),
        Row(
          children: [
            ElevatedButton.icon(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('点击了确认按钮')),
                );
              },
              icon: const Icon(Icons.check),
              label: const Text('确认'),
            ),
            const SizedBox(width: 12),
            OutlinedButton.icon(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('点击了取消按钮')),
                );
              },
              icon: const Icon(Icons.close),
              label: const Text('取消'),
            ),
          ],
        ),
      ],
    ),
  ),
)

2.3 点击反馈效果

不同状态的点击反馈对比:

状态类型 实现方式 视觉效果 触觉反馈
点击波纹 InkWell 水波纹扩散 轻微震动
按下效果 Listener 颜色变化
悬停效果 MouseRegion 边框高亮
长按效果 GestureDetector 缩放效果 震动
Card(
  child: Material(
    child: InkWell(
      onTap: () {
        print('Card被点击');
      },
      onTapDown: (_) {
        print('按下');
      },
      onTapUp: (_) {
        print('抬起');
      },
      onLongPress: () {
        print('长按');
      },
      splashColor: Theme.of(context).primaryColor.withOpacity(0.3),
      highlightColor: Theme.of(context).primaryColor.withOpacity(0.1),
      child: const Padding(
        padding: EdgeInsets.all(16),
        child: Text('带完整点击反馈的Card'),
      ),
    ),
  ),
)

三、滑动交互

3.1 Dismissible可滑动删除

使用Dismissible实现滑动操作:

class DismissibleCard extends StatelessWidget {
  final String title;
  final VoidCallback onDelete;

  const DismissibleCard({
    super.key,
    required this.title,
    required this.onDelete,
  });

  
  Widget build(BuildContext context) {
    return Dismissible(
      key: Key(title),
      onDismissed: (direction) {
        onDelete();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('$title 已删除')),
        );
      },
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.only(right: 20),
        child: const Icon(
          Icons.delete,
          color: Colors.white,
        ),
      ),
      secondaryBackground: Container(
        color: Colors.blue,
        alignment: Alignment.centerLeft,
        padding: const EdgeInsets.only(left: 20),
        child: const Icon(
          Icons.archive,
          color: Colors.white,
        ),
      ),
      child: Card(
        child: ListTile(
          title: Text(title),
          trailing: const Icon(Icons.drag_handle),
        ),
      ),
    );
  }
}

3.2 滑动操作按钮

实现左右滑动显示操作按钮:

Card

左滑显示删除

右滑显示归档

上滑显示置顶

下滑显示分享

红色背景

蓝色背景

绿色背景

橙色背景

class SwipeableCard extends StatefulWidget {
  final String title;

  const SwipeableCard({super.key, required this.title});

  
  State<SwipeableCard> createState() => _SwipeableCardState();
}

class _SwipeableCardState extends State<SwipeableCard> {
  bool _isExpanded = false;

  
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 背景操作按钮
        Positioned.fill(
          child: Row(
            children: [
              Expanded(
                child: Container(
                  color: Colors.red,
                  child: const Icon(
                    Icons.delete,
                    color: Colors.white,
                  ),
                ),
              ),
              Expanded(
                child: Container(
                  color: Colors.blue,
                  child: const Icon(
                    Icons.archive,
                    color: Colors.white,
                  ),
                ),
              ),
            ],
          ),
        ),
        // 前景卡片
        Dismissible(
          key: Key(widget.title),
          onDismissed: (direction) {
            if (direction == DismissDirection.startToEnd) {
              print('归档');
            } else {
              print('删除');
            }
          },
          child: Card(
            child: ListTile(
              title: Text(widget.title),
              subtitle: Text('左滑归档,右滑删除'),
              trailing: const Icon(Icons.drag_handle),
            ),
          ),
        ),
      ],
    );
  }
}

四、拖拽交互

4.1 Draggable可拖拽卡片

实现可拖拽的Card:

class DraggableCard extends StatelessWidget {
  final String title;
  final String data;

  const DraggableCard({
    super.key,
    required this.title,
    required this.data,
  });

  
  Widget build(BuildContext context) {
    return Draggable<String>(
      data: data,
      feedback: Material(
        elevation: 8,
        borderRadius: BorderRadius.circular(8),
        child: Card(
          child: Container(
            width: 200,
            padding: const EdgeInsets.all(16),
            child: Text(
              title,
              style: const TextStyle(color: Colors.black),
            ),
          ),
        ),
      ),
      childWhenDragging: Card(
        color: Colors.grey.shade300,
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Text(
            title,
            style: TextStyle(
              color: Colors.grey.shade600,
            ),
          ),
        ),
      ),
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              const Icon(Icons.drag_handle),
              const SizedBox(width: 12),
              Expanded(
                child: Text(title),
              ),
              const Icon(Icons.more_vert),
            ],
          ),
        ),
      ),
    );
  }
}

4.2 DragTarget放置目标

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

  
  State<DropZone> createState() => _DropZoneState();
}

class _DropZoneState extends State<DropZone> {
  final List<String> _items = [];
  bool _isDraggingOver = false;

  
  Widget build(BuildContext context) {
    return DragTarget<String>(
      onAccept: (data) {
        setState(() {
          _items.add(data);
          _isDraggingOver = false;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('$data 已添加')),
        );
      },
      onWillAccept: (data) {
        setState(() {
          _isDraggingOver = true;
        });
        return true;
      },
      onLeave: (data) {
        setState(() {
          _isDraggingOver = false;
        });
      },
      builder: (context, candidateData, rejectedData) {
        return Card(
          color: _isDraggingOver
              ? Colors.green.shade200
              : Colors.grey.shade100,
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              children: [
                Icon(
                  _isDraggingOver ? Icons.cloud_upload : Icons.cloud_download,
                  size: 48,
                  color: _isDraggingOver ? Colors.green : Colors.grey,
                ),
                const SizedBox(height: 12),
                Text(
                  _isDraggingOver ? '松开添加' : '拖拽到这里',
                  style: TextStyle(
                    color: _isDraggingOver ? Colors.green : Colors.grey.shade600,
                    fontSize: 16,
                  ),
                ),
                if (_items.isNotEmpty) ...[
                  const SizedBox(height: 16),
                  const Divider(),
                  ..._items.map((item) => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 4),
                        child: Text('• $item'),
                      )),
                ],
              ],
            ),
          ),
        );
      },
    );
  }
}

五、展开折叠交互

5.1 ExpansionTile卡片

使用ExpansionTile实现可展开的Card:

class ExpandableCard extends StatelessWidget {
  final String title;
  final String description;
  final List<String> details;

  const ExpandableCard({
    super.key,
    required this.title,
    required this.description,
    required this.details,
  });

  
  Widget build(BuildContext context) {
    return Card(
      child: ExpansionTile(
        title: Text(
          title,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
        subtitle: Text(description),
        leading: const Icon(Icons.expand_more),
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: details
                  .map((detail) => Padding(
                        padding: const EdgeInsets.only(bottom: 8),
                        child: Row(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            const Text('• ', style: TextStyle(fontSize: 16)),
                            Expanded(child: Text(detail)),
                          ],
                        ),
                      ))
                  .toList(),
            ),
          ),
        ],
      ),
    );
  }
}

5.2 动画展开效果

自定义展开动画:

class AnimatedExpandableCard extends StatefulWidget {
  final String title;
  final String content;

  const AnimatedExpandableCard({
    super.key,
    required this.title,
    required this.content,
  });

  
  State<AnimatedExpandableCard> createState() => _AnimatedExpandableCardState();
}

class _AnimatedExpandableCardState extends State<AnimatedExpandableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  bool _isExpanded = false;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );
  }

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

  void _toggleExpand() {
    setState(() {
      _isExpanded = !_isExpanded;
      if (_isExpanded) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          ListTile(
            title: Text(widget.title),
            trailing: AnimatedRotation(
              turns: _animation.value * 0.5,
              duration: const Duration(milliseconds: 300),
              child: const Icon(Icons.expand_more),
            ),
            onTap: _toggleExpand,
          ),
          SizeTransition(
            sizeFactor: _animation,
            axisAlignment: -1,
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Text(widget.content),
            ),
          ),
        ],
      ),
    );
  }
}

六、多选交互

6.1 带复选框的Card

class SelectableCard extends StatefulWidget {
  final String title;
  final ValueChanged<bool?> onSelected;

  const SelectableCard({
    super.key,
    required this.title,
    required this.onSelected,
  });

  
  State<SelectableCard> createState() => _SelectableCardState();
}

class _SelectableCardState extends State<SelectableCard> {
  bool _isSelected = false;

  
  Widget build(BuildContext context) {
    return Card(
      color: _isSelected ? Colors.blue.shade50 : null,
      child: CheckboxListTile(
        value: _isSelected,
        onChanged: (value) {
          setState(() {
            _isSelected = value ?? false;
          });
          widget.onSelected(value);
        },
        title: Text(
          widget.title,
          style: TextStyle(
            fontWeight: _isSelected ? FontWeight.bold : FontWeight.normal,
          ),
        ),
        subtitle: Text(_isSelected ? '已选中' : '点击选择'),
        controlAffinity: ListTileControlAffinity.leading,
      ),
    );
  }
}

6.2 批量操作模式

class BatchSelectionCard extends StatefulWidget {
  final String title;

  const BatchSelectionCard({super.key, required this.title});

  
  State<BatchSelectionCard> createState() => _BatchSelectionCardState();
}

class _BatchSelectionCardState extends State<BatchSelectionCard> {
  bool _isInSelectionMode = false;
  bool _isSelected = false;

  
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: _isInSelectionMode
            ? Checkbox(
                value: _isSelected,
                onChanged: (value) {
                  setState(() {
                    _isSelected = value ?? false;
                  });
                },
              )
            : const Icon(Icons.description),
        title: Text(widget.title),
        subtitle: Text(_isInSelectionMode ? '批量操作模式' : '长按进入批量操作'),
        trailing: _isInSelectionMode
            ? null
            : const Icon(Icons.more_vert),
        onLongPress: () {
          setState(() {
            _isInSelectionMode = true;
          });
        },
      ),
    );
  }
}

七、手势识别

7.1 多手势支持

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

  
  Widget build(BuildContext context) {
    return Card(
      child: GestureDetector(
        onTap: () {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('点击')),
          );
        },
        onDoubleTap: () {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('双击')),
          );
        },
        onLongPress: () {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('长按')),
          );
        },
        onHorizontalDragEnd: (details) {
          if (details.primaryVelocity != null) {
            if (details.primaryVelocity! > 0) {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('向右滑动')),
              );
            } else {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('向左滑动')),
              );
            }
          }
        },
        child: const Padding(
          padding: EdgeInsets.all(16),
          child: Text('支持点击、双击、长按、滑动'),
        ),
      ),
    );
  }
}

7.2 手势冲突处理

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

  
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        // 长按手势优先级最高
        LongPressGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
          () => LongPressGestureRecognizer(duration: const Duration(milliseconds: 500)),
          (LongPressGestureRecognizer instance) {
            instance.onLongPress = () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('长按操作')),
              );
            };
          },
        ),
        // 点击手势
        TapGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
          () => TapGestureRecognizer(),
          (TapGestureRecognizer instance) {
            instance.onTap = () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('点击操作')),
              );
            };
          },
        ),
      },
      child: const Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Text('手势冲突处理示例'),
        ),
      ),
    );
  }
}

八、完整交互示例

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Card交互效果'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Text(
            '点击交互',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),
          InkWell(
            onTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Card被点击了')),
              );
            },
            child: Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    const Icon(Icons.touch_app, color: Colors.blue),
                    const SizedBox(width: 12),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            '可点击Card',
                            style: TextStyle(fontWeight: FontWeight.bold),
                          ),
                          Text('点击整个Card', style: TextStyle(color: Colors.grey.shade600)),
                        ],
                      ),
                    ),
                    const Icon(Icons.arrow_forward_ios),
                  ],
                ),
              ),
            ),
          ),
          const SizedBox(height: 16),
          
          const Text(
            '按钮交互',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    '带操作按钮的Card',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      ElevatedButton.icon(
                        onPressed: () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('确认')),
                          );
                        },
                        icon: const Icon(Icons.check),
                        label: const Text('确认'),
                      ),
                      const SizedBox(width: 12),
                      OutlinedButton.icon(
                        onPressed: () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('取消')),
                          );
                        },
                        icon: const Icon(Icons.close),
                        label: const Text('取消'),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),

          const Text(
            '展开折叠',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),
          Card(
            child: ExpansionTile(
              title: const Text(
                '可展开的Card',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              subtitle: const Text('点击展开查看详情'),
              children: [
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: const [
                      Text('• 展开内容第一项'),
                      Text('• 展开内容第二项'),
                      Text('• 展开内容第三项'),
                    ],
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 16),

          const Text(
            '复选框交互',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),
          Card(
            child: CheckboxListTile(
              value: false,
              onChanged: (value) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(value == true ? '已选中' : '未选中')),
                );
              },
              title: const Text('可选择的Card'),
              subtitle: const Text('点击复选框选择'),
              controlAffinity: ListTileControlAffinity.leading,
            ),
          ),
          const SizedBox(height: 16),

          const Text(
            '手势识别',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),
          GestureDetector(
            onTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('单次点击')),
              );
            },
            onDoubleTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('双击')),
              );
            },
            onLongPress: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('长按')),
              );
            },
            child: Card(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  children: [
                    const Icon(Icons.gesture, size: 48, color: Colors.blue),
                    const SizedBox(height: 12),
                    const Text(
                      '多手势Card',
                      style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    Text('支持单击、双击、长按', style: TextStyle(color: Colors.grey.shade600)),
                  ],
                ),
              ),
            ),
          ),
          const SizedBox(height: 16),

          const Text(
            '拖拽卡片',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),
          DraggableCard(title: '拖拽我', data: '可拖拽数据'),
        ],
      ),
    );
  }
}

九、交互设计最佳实践

交互设计原则

原则 说明 实例
明确性 交互意图清晰 使用图标+文字
反馈性 即时视觉反馈 点击波纹效果
一致性 保持风格统一 所有Card相同阴影
可预测 符合用户习惯 点击查看详情
可撤销 允许误操作恢复 撤销删除操作

交互性能优化

// ✅ 使用const构造函数
const Card(
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Text('静态内容'),
  ),
)

// ✅ 避免在build中创建新对象
class MyCard extends StatelessWidget {
  final String title;
  
  const MyCard({super.key, required this.title});
  
  
  Widget build(BuildContext context) {
    return Card(
      child: Text(title),
    );
  }
}

// ❌ 避免不必要的重建
class BadCard extends StatefulWidget {
  const BadCard({super.key});
  
  
  State<BadCard> createState() => _BadCardState();
}

class _BadCardState extends State<BadCard> {
  // ❌ 避免在build中创建新Widget
  
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
        onTap: () {
          // ❌ 避免在build中定义函数
          print('tapped');
        },
        child: const Text('Click'),
      ),
    );
  }
}

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐