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

Flutter实现可滑动接受/拒绝请求的SliderButton拖拽限制问题

实现方案(用GestureDetector替代Draggable)

核心逻辑是通过GestureDetector的Pan系列手势监听拖拽行为,配合Stack+Positioned控制按钮位置,同时手动做边界校验,完全可以满足限制在容器内拖动的需求。

关键修改点

  • 布局改用Stack承载左右提示文本和可拖拽按钮,按钮位置通过偏移量动态控制
  • 拖拽时实时计算偏移,新增边界限制:按钮左边缘最小不能小于0,右边缘最大不能超过容器宽度
  • 拖动阈值计算改为相对容器宽度的比例,适配不同尺寸的容器和屏幕
  • 新增拖拽结束未达阈值时自动回弹到初始居中位置的逻辑
  • 修复原代码中左右回调逻辑混用的问题,左侧拒绝触发次函数,右侧接受触发主函数

修改后的完整代码

import 'package:flutter/material.dart';

class SliderButton extends StatefulWidget {
  final double containerWidth;
  final double containerHeight;
  final Color containerColor;

  final double buttonSize;
  final Color buttonColor;

  final Color textColor;
  final double textSize;

  final String leftText;
  final String rightText;

  final String textResultDeny;
  final String textResultAccept;

  final IconData icon;
  final Color iconColor;

  final Function(BuildContext context)? sliderPrimaryFunction;
  final Function(BuildContext context)? sliderSecondaryFunction;

  SliderButton(
      {required this.containerWidth,
      required this.containerHeight,
      this.containerColor = Colors.black,
      this.buttonSize = 50,
      this.buttonColor = Colors.white,
      this.rightText = '接受',
      this.textResultAccept = '已接受',
      this.leftText = '拒绝',
      this.textResultDeny = '已拒绝',
      this.sliderPrimaryFunction,
      this.sliderSecondaryFunction,
      this.textColor = Colors.white,
      this.textSize = 24,
      this.icon = Icons.add_circle_outline,
      this.iconColor = Colors.black});

  @override
  _SliderButtonState createState() => _SliderButtonState();
}

class _SliderButtonState extends State<SliderButton> {
  // 按钮水平偏移量
  late double _dx;
  // 初始居中偏移
  late double _initialDx;
  int switchOptions = 0;
  Color? _containerColor = Colors.black;

  @override
  void initState() {
    super.initState();
    // 计算初始居中位置:(容器宽度 - 按钮宽度)/2
    _initialDx = (widget.containerWidth - widget.buttonSize) / 2;
    _dx = _initialDx;
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: sliderContainer(),
    );
  }

  sliderContainer() => Container(
      width: widget.containerWidth,
      height: widget.containerHeight,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(50.00),
        color: _containerColor,
        boxShadow: const [
          BoxShadow(
            color: Colors.black,
            offset: Offset(0.0, 1.0),
            blurRadius: 6.0,
          ),
        ],
      ),
      child: sliderContainerContent());

  sliderContainerContent() {
    if (switchOptions == 0) return containerContent();
    if (switchOptions == 1) {
      return textResult(widget.textResultAccept);
    } else if (switchOptions == 2) {
      return textResult(widget.textResultDeny);
    }
    return Container();
  }

  containerContent() => Stack(
    alignment: Alignment.center,
    children: [
      // 左侧拒绝文本
      Positioned(
        left: 14,
        child: primaryText(widget.leftText),
      ),
      // 右侧接受文本
      Positioned(
        right: 14,
        child: primaryText(widget.rightText),
      ),
      // 可拖拽按钮
      Positioned(
        left: _dx,
        child: GestureDetector(
          onPanStart: (details) {
            // 拖动开始时重置容器颜色
            setState(() {
              _containerColor = widget.containerColor;
            });
          },
          onPanUpdate: (details) {
            setState(() {
              // 累加偏移量
              _dx += details.delta.dx;
              // 边界限制:不能小于0,不能大于容器宽度-按钮宽度
              _dx = _dx.clamp(0, widget.containerWidth - widget.buttonSize);
              // 实时更新容器颜色
              _updateContainerColor();
            });
          },
          onPanEnd: (details) {
            _handleDragEnd();
          },
          child: roundedButton(),
        ),
      )
    ],
  );

  roundedButton() => Container(
    width: widget.buttonSize,
    height: widget.buttonSize,
    decoration: BoxDecoration(
      shape: BoxShape.circle,
      color: widget.buttonColor,
      boxShadow: const [
        BoxShadow(
          color: Colors.grey,
          offset: Offset(0.0, 1.0),
          blurRadius: 6.0,
        ),
      ],
    ),
    child: Icon(widget.icon,
        color: widget.iconColor, size: widget.buttonSize * 0.6),
  );

  primaryText(String text) => Text(
    text,
    style: Theme.of(context)
        .textTheme
        .headline5
        ?.copyWith(color: widget.textColor),
  );

  textResult(String text) => Center(
      child: Text(text,
          style: Theme.of(context)
              .textTheme
              .headline3
              ?.copyWith(color: Colors.white)));

  void _updateContainerColor() {
    // 左滑比例:0在最右,1在最左
    final leftRatio = (widget.containerWidth - widget.buttonSize - _dx) / (widget.containerWidth - widget.buttonSize);
    // 右滑比例:0在最左,1在最右
    final rightRatio = _dx / (widget.containerWidth - widget.buttonSize);

    if (rightRatio > 0.5) {
      // 右滑超过一半,渐变绿色
      final greenShade = (200 + rightRatio * 300).toInt().clamp(200, 900);
      _containerColor = Colors.green[greenShade];
    } else if (leftRatio > 0.5) {
      // 左滑超过一半,渐变红色
      final redShade = (200 + leftRatio * 300).toInt().clamp(200, 900);
      _containerColor = Colors.red[redShade];
    } else {
      _containerColor = widget.containerColor;
    }
  }

  void _handleDragEnd() {
    // 右滑阈值:超过容器宽度2/3判定为接受
    final acceptThreshold = (widget.containerWidth - widget.buttonSize) * 2 / 3;
    // 左滑阈值:小于容器宽度1/3判定为拒绝
    final denyThreshold = (widget.containerWidth - widget.buttonSize) / 3;

    if (_dx >= acceptThreshold) {
      setState(() {
        switchOptions = 1;
        _containerColor = Colors.lightGreen;
      });
      Future.delayed(const Duration(milliseconds: 500), () {
        if (widget.sliderPrimaryFunction != null) {
          widget.sliderPrimaryFunction!(context);
        } else {
          Navigator.pop(context);
        }
      });
    } else if (_dx <= denyThreshold) {
      setState(() {
        switchOptions = 2;
        _containerColor = Theme.of(context).errorColor;
      });
      Future.delayed(const Duration(milliseconds: 500), () {
        if (widget.sliderSecondaryFunction != null) {
          widget.sliderSecondaryFunction!(context);
        } else {
          Navigator.pop(context);
        }
      });
    } else {
      // 未达阈值,回弹到初始位置
      setState(() {
        _dx = _initialDx;
        _containerColor = widget.containerColor;
      });
    }
  }
}

注意事项

  • 实例化组件时直接传入容器宽高即可,按钮会默认居中显示
  • 阈值比例可以根据需求自行调整,当前设置是右滑超过2/3触发接受,左滑小于1/3触发拒绝
  • 如果需要更流畅的回弹效果,可以额外引入AnimationController给偏移量加动画过渡

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 01:48:03