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

Flutter Stack组件在不同位置的表现差异及点击失效问题

问题:自定义扩展式FloatingActionButton在Scaffold中无法点击子选项

我实现了一个带扩展功能的FloatingActionButton,点击主按钮后,Stack内的多个InkWell选项会展开显示。但遇到了异常情况:

正常工作的场景

当将自定义MyFAB组件作为MaterialApp的home选项时,动画运行正常,子InkWell组件可正常点击:

import 'package:flutter/material.dart';
import 'dart:math';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:  MyFAB(),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

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

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

异常场景

当将MyFAB作为Scaffold的floatingActionButton时,展开的小图标无法被点击:

import 'package:flutter/material.dart';
import 'dart:math';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: MyFAB(),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

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

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

原因分析

问题根源在于Scaffold对floatingActionButton的布局约束:Scaffold会为floatingActionButton分配一个固定的布局范围(即主FAB的尺寸),即使你通过Transform将子选项移到这个范围之外,这些选项的点击响应区域仍然被限制在原始范围内,导致外部的点击无法触发。

解决方法

给MyFAB的外层包裹OverflowBox,突破Scaffold的布局约束,让子组件的点击区域可以扩展到原始范围之外。修改MyFAB的build方法如下:

@override
Widget build(BuildContext context) {
  return OverflowBox(
    maxWidth: double.infinity,
    maxHeight: double.infinity,
    child: Stack(
      alignment: Alignment.center,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    ),
  );
}

OverflowBox允许子组件超出父组件的布局边界,同时保留完整的点击响应区域,这样展开的子选项就能正常接收点击事件了。

另一种可选方案是:不在Scaffold的floatingActionButton参数中传入MyFAB,而是在Scaffold的body里添加一个Stack,将MyFAB放在Stack的底部中心位置,这种方式也能避开Scaffold的约束限制,但前者更贴合floatingActionButton的使用场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 08:44:56