You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:修复Flutter Dismissible拖拽时的圆角白边问题

修复Flutter Dismissible卡片拖拽时的圆角白边问题

问题分析

拖拽时出现的圆角白边,主要源于多层容器的圆角裁剪未完全对齐、手动计算高度存在误差,以及背景与前景的圆角逻辑不一致导致的视觉缝隙。

修复方案

以下是调整后的完整代码,关键优化点:

  • 统一用ClipRRect包裹整个卡片内容,确保前景和背景的圆角完全匹配
  • 用LayoutBuilder替代手动获取高度,避免计算误差
  • 复用统一的圆角配置,消除背景与前景的样式差异
  • 使用Dismissible内置的onUpdate回调获取拖拽偏移,替代Listener提升准确性
class InvitationCard extends StatefulWidget {
  final GameInvitation invitation;
  final void Function(GameInvitation) onAccept;
  final void Function(GameInvitation) onDecline;

  const InvitationCard({super.key, required this.invitation, required this.onAccept, required this.onDecline});

  @override
  State<InvitationCard> createState() => _InvitationCardState();
}

class _InvitationCardState extends State<InvitationCard> {
  double swipeOffset = 0;

  Color overlayedColor = Colors.transparent;
  EdgeInsets padding = const EdgeInsets.all(8.0);
  double elevation = 1.5;
  BorderRadius borderRadius = BorderRadius.circular(30);
  Alignment iconAlignment = Alignment.centerLeft;

  void _updateSwipeState(double offset) {
    setState(() {
      swipeOffset = offset;
      final screenWidth = MediaQuery.of(context).size.width;
      final swipeRatio = (offset.abs() / screenWidth).clamp(0.3, 1.0);

      if (offset == 0) {
        padding = const EdgeInsets.all(8.0);
        elevation = 1.5;
        borderRadius = BorderRadius.circular(30);
        overlayedColor = Colors.transparent;
        iconAlignment = Alignment.centerLeft;
      } else if (offset > 0) {
        padding = const EdgeInsets.only(top: 8.0, bottom: 8.0);
        elevation = 0;
        borderRadius = offset.abs() < 30 
            ? BorderRadius.circular(30)
            : const BorderRadius.only(topRight: Radius.circular(30), bottomRight: Radius.circular(30));
        overlayedColor = Colors.green.withOpacity(swipeRatio);
        iconAlignment = Alignment.centerLeft;
      } else {
        padding = const EdgeInsets.only(top: 8.0, bottom: 8.0);
        elevation = 0;
        borderRadius = offset.abs() < 30 
            ? BorderRadius.circular(30)
            : const BorderRadius.only(topLeft: Radius.circular(30), bottomLeft: Radius.circular(30));
        overlayedColor = Colors.red.withOpacity(swipeRatio);
        iconAlignment = Alignment.centerRight;
      }
    });
  }

  Widget _buildSwipeBackground(Color color, Alignment alignment, IconData icon) {
    return Padding(
      padding: padding,
      child: ClipRRect(
        borderRadius: borderRadius,
        child: Container(
          alignment: alignment,
          padding: const EdgeInsets.symmetric(horizontal: 20),
          color: color.withOpacity((swipeOffset.abs() / MediaQuery.of(context).size.width).clamp(0.3, 1.0)),
          child: Icon(icon, color: Colors.white, size: 30),
        ),
      ),
    );
  }

  Widget _roundTypeBox(IconData icon, String typeName) {
    return Container(
      padding: const EdgeInsets.all(4),
      margin: const EdgeInsets.only(right: 4),
      decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary, borderRadius: BorderRadius.circular(8)),
      child: Row(
        children: [
          Icon(icon, size: 16, color: Colors.grey),
          const SizedBox(width: 8),
          Text(typeName, style: TextStyle(color: Colors.grey, fontSize: 16, fontWeight: FontWeight.bold)),
        ],
      ),
    );
  }

  Widget _invitationContent(GameInvitation invitation, BoxConstraints constraints) {
    List<String> gameTypes = [];
    List<IconData> icons = [];
    if (invitation.isMultipleChoiceSelected) {
      gameTypes.add("Multiple Choice");
      icons.add(Icons.tile_small);
    }
    if (invitation.isEstimatingSelected) {
      gameTypes.add("Schätzfragen");
      icons.add(Icons.sliders);
    }

    return Stack(
      children: [
        Material(
          elevation: elevation,
          shadowColor: Theme.of(context).shadowColor,
          borderRadius: borderRadius,
          child: Container(
            width: constraints.maxWidth,
            decoration: BoxDecoration(color: Theme.of(context).colorScheme.primary, borderRadius: borderRadius),
            child: ListTile(
              title: Row(
                children: [
                  // 替换为你的roundProfileImage实现
                  Container(width:50, height:50, decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.grey)),
                  const SizedBox(width: 10),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Container(
                            padding: const EdgeInsets.all(4),
                            decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary, borderRadius: BorderRadius.circular(8)),
                            child: Text(invitation.host, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                          ),
                          const SizedBox(width: 10),
                          Text(invitation.timestamp.timeAgo, style: TextStyle(fontSize: 16, color: Colors.grey, fontWeight: FontWeight.bold)),
                        ],
                      ),
                      Text("${invitation.roundsCount} Runden", style: TextStyle(fontSize: 16, color: Colors.grey, fontWeight: FontWeight.bold)),
                      Row(
                        children: [
                          ...List.generate(gameTypes.length, (index) => _roundTypeBox(icons[index], gameTypes[index])),
                          const SizedBox(width: 10),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
        ClipRRect(
          borderRadius: borderRadius,
          child: Container(
            width: constraints.maxWidth,
            height: constraints.maxHeight,
            alignment: iconAlignment,
            color: overlayedColor,
            padding: const EdgeInsets.symmetric(horizontal: 20),
            child: swipeOffset != 0 
                ? Icon(swipeOffset > 0 ? Icons.check : Icons.delete, color: Colors.white, size: 30)
                : null,
          ),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      key: ValueKey(widget.invitation.id),
      direction: DismissDirection.horizontal,
      background: _buildSwipeBackground(Colors.green, Alignment.centerLeft, Icons.check),
      secondaryBackground: _buildSwipeBackground(Colors.red, Alignment.centerRight, Icons.delete),
      confirmDismiss: (direction) async {
        if (direction == DismissDirection.startToEnd) {
          widget.onAccept(widget.invitation);
        } else {
          widget.onDecline(widget.invitation);
        }
        return false;
      },
      onUpdate: (details) => _updateSwipeState(details.progress * MediaQuery.of(context).size.width * (details.direction == DismissDirection.startToEnd ? 1 : -1)),
      child: Padding(
        padding: padding,
        child: ClipRRect(
          borderRadius: borderRadius,
          child: LayoutBuilder(
            builder: (context, constraints) {
              return _invitationContent(widget.invitation, constraints);
            },
          ),
        ),
      ),
    );
  }
}

关键优化说明

  1. 统一裁剪层级:用外层ClipRRect包裹整个卡片,确保所有内容都遵循同一个圆角规则,避免多层裁剪产生缝隙
  2. 动态尺寸获取:通过LayoutBuilder直接获取容器尺寸,替代手动计算高度,彻底消除尺寸误差
  3. 复用样式配置:将背景和前景的圆角、颜色逻辑统一,确保拖拽时视觉完全同步
  4. 精准拖拽监听:使用Dismissible.onUpdate获取拖拽进度,比Listener更准确,能实时同步背景和前景的状态

内容的提问来源于stack exchange,提问作者soosmann

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 08:29:56