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

Flutter:Positioned组件内按钮onPressed失效问题求助

语音消息锁定模式下Positioned组件内按钮点击无效问题解决

我正在实现类似WhatsApp的带语音消息功能的聊天框,语音消息UI显示正常,但录音锁定模式下,Positioned组件内的删除、发送、暂停按钮点击无效,点击时反而触发下方消息输入框弹出键盘。需要解决这个问题,让按钮正常响应。

相关代码

main.dart

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
late AnimationController controller;

@override
void initState() {
    super.initState();
    controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 600),
    );
}

@override
void dispose() {
    controller.dispose();
    super.dispose();
}

@override
Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
        title: const Text("Audio Chat"),
    ),
    body: Padding(
        padding: const EdgeInsets.all(Globals.defaultPadding),
        child: Column(
        children: [
            const Expanded(child: AudioList()),
            Row(
            mainAxisSize: MainAxisSize.max,
            children: [
                ChatBox(controller: controller),
                const SizedBox(width: 4),
                RecordButton(controller: controller),
            ],
            ),
        ],
        ),
    ),
    );
}
}

record_button.dart

class _RecordButtonState extends State<RecordButton> {
static const double size = 55;

final double lockerHeight = 200;
double timerWidth = 0;

late Animation<double> buttonScaleAnimation;
late Animation<double> timerAnimation;
late Animation<double> lockerAnimation;

DateTime? startTime;
Timer? timer;
String recordDuration = "00:00";
late Record record;

bool isLocked = false;
bool showLottie = false;

@override
void initState() {
super.initState();
buttonScaleAnimation = Tween<double>(begin: 1, end: 2).animate(
  CurvedAnimation(
    parent: widget.controller,
    curve: const Interval(0.0, 0.6, curve: Curves.elasticInOut),
  ),
);
widget.controller.addListener(() {
  setState(() {});
});
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
timerWidth = MediaQuery.of(context).size.width - 2 * Globals.defaultPadding - 4 + 5;
timerAnimation =
    Tween<double>(begin: timerWidth + Globals.defaultPadding, end: 0)
        .animate(
      CurvedAnimation(
        parent: widget.controller,
        curve: const Interval(0.2, 1, curve: Curves.easeIn),
      ),
    );
lockerAnimation =
    Tween<double>(begin: lockerHeight + Globals.defaultPadding, end: 0)
        .animate(
      CurvedAnimation(
        parent: widget.controller,
        curve: const Interval(0.2, 1, curve: Curves.easeIn),
      ),
    );
}

@override
void dispose() {
record.dispose();
timer?.cancel();
timer = null;
super.dispose();
}

@override
Widget build(BuildContext context) {
return Stack(
  clipBehavior: Clip.none,
  children: [
    lockSlider(),
    cancelSlider(),
    audioButton(),
    if (isLocked) timerLocked(),
  ],
);
}

Widget lockSlider() {
return Positioned(
  bottom: -lockerAnimation.value,
  child: Container(
    height: lockerHeight,
    width: size,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(Globals.borderRadius),
      color: Colors.black,
    ),
    padding: const EdgeInsets.symmetric(vertical: 15),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        const Icon(FontAwesomeIcons.lock, size: 20),
        const SizedBox(height: 8),
        FlowShader(
          direction: Axis.vertical,
          child: Column(
            children: const [
              Icon(Icons.keyboard_arrow_up),
              Icon(Icons.keyboard_arrow_up),
              Icon(Icons.keyboard_arrow_up),
            ],
          ),
        ),
      ],
    ),
  ),
);
}

Widget cancelSlider() {
return Positioned(
  right: -timerAnimation.value,
  child: Container(
    height: size,
    width: timerWidth,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(Globals.borderRadius),
      color: Colors.black,
    ),
    child: Padding(
      padding: const EdgeInsets.symmetric(horizontal: 15),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        mainAxisSize: MainAxisSize.max,
        children: [
          showLottie ? const LottieAnimation() : Text(recordDuration),
          const SizedBox(width: size),
          FlowShader(
            child: Row(
              children: const [
                Icon(Icons.keyboard_arrow_left),
                Text("Slide to cancel")
              ],
            ),
            duration: const Duration(seconds: 3),
            flowColors: const [Colors.white, Colors.grey],
          ),
          const SizedBox(width: size),
        ],
      ),
    ),
  ),
);
}

Widget timerLocked() {
return Positioned(
  right: 0,
  bottom: 0,
  child: Container(
    height: 80,
    width: timerWidth,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(Globals.borderRadius - 10),
      color: Colors.lightBlue,
    ),
    child: Padding(
      padding: const EdgeInsets.only(left: 15, right: 25),
      child: Column(
        children: [
          Flexible(
            flex: 1,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              mainAxisSize: MainAxisSize.max,
              children: [
                Text(recordDuration),
                FlowShader(
                  child: const Text("Tap lock to stop"),
                  duration: const Duration(seconds: 3),
                  flowColors: const [Colors.white, Colors.grey],
                ),
                GestureDetector(
                  behavior: HitTestBehavior.opaque,
                  onTap: () async {
                    print('Recording finished');
                  },
                  child: const Center(
                    child: Icon(
                      FontAwesomeIcons.check,
                      size: 18,
                      color: Colors.black,
                    ),
                  ),
                ),
              ],
            ),
          ),
          Flexible(
            flex: 1,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              mainAxisSize: MainAxisSize.max,
              children: [
                Center(
                  child: InkWell(
                    onTap: () {
                      print('Action Delete');
                    },
                    child: Icon(FontAwesomeIcons.trash,size: 18,color: Colors.black,),
                  ),
                ),
                GestureDetector(
                  behavior: HitTestBehavior.opaque,
                  onTap: () async {
                    print('Action Pause');
                  },
                  child: const Center(
                    child: Icon(
                      FontAwesomeIcons.pause,
                      size: 18,
                      color: Colors.black,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  ),
);
}

Widget audioButton() {
return GestureDetector(
  child: Transform.scale(
    scale: buttonScaleAnimation.value,
    child: Container(
      child: const Icon(Icons.mic),
      height: size,
      width: size,
      clipBehavior: Clip.hardEdge,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: Theme.of(context).primaryColor,
      ),
    ),
  ),
  onLongPressDown: (_) {
    widget.controller.forward();
  },
  onLongPressEnd: (details) async {

    if (isCancelled(details.localPosition, context)) {

    } else if (checkIsLocked(details.localPosition)) {
      widget.controller.reverse();

      Vibrate.feedback(FeedbackType.heavy);
      setState(() {
        isLocked = true;
      });
    } else {
      print('Recording Finished');
    }
  },
  onLongPressCancel: () {
    widget.controller.reverse();
  },
  onLongPress: () async {
    Vibrate.feedback(FeedbackType.success);
    if (await Record().hasPermission()) {
      record = Record();
      await record.start(
        path: Globals.documentPath +
            "audio_${DateTime.now().millisecondsSinceEpoch}.m4a",
        encoder: AudioEncoder.aacEld,
        bitRate: 128000,
        samplingRate: 44100,
      );
      startTime = DateTime.now();
      timer = Timer.periodic(const Duration(seconds: 1), (_) {
        final minDur = DateTime.now().difference(startTime!).inMinutes;
        final secDur = DateTime.now().difference(startTime!).inSeconds % 60;
        String min = minDur < 10 ? "0$minDur" : minDur.toString();
        String sec = secDur < 10 ? "0$secDur" : secDur.toString();
        setState(() {
          recordDuration = "$min:$sec";
        });
      });
    }
  },
);
}

bool checkIsLocked(Offset offset) {
return (offset.dy < -35);
}

bool isCancelled(Offset offset, BuildContext context) {
return (offset.dx < -(MediaQuery.of(context).size.width * 0.2));
}
}

临时解决方案

通过将timerLocked的Positioned组件嵌套在Stack和SizedBox中,解决按钮点击无效的问题。修改后的timerLocked方法代码如下:

Widget timerLocked() {
return Positioned(
  child: SizedBox(
    height: 80,
    width: timerWidth,
    child: Stack(
      children: [
        Positioned(
          right: 0,
          bottom: 0,
          height: 80,
          width: timerWidth,
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(Globals.borderRadius - 10),
              color: Colors.lightBlue,
            ),
            child: Padding(
              padding: const EdgeInsets.only(left: 15, right: 25),
              child: Column(
                children: [
                  // 原有的按钮和文本内容
                ],
              ),
            ),
          ),
        ),
      ],
    ),
  ),
);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 11:02:00