'GestureDetector does not work after Transform and Offset Animation (Flutter)

I want to create an spinning animation with a set of buttons - arranged in a circle - that rotate and extend from a clicked center button.

While animation and rotation work well, and the open button and close button work smoothly and respond to onTap() in spite of rotation, the outer buttons do not work in terms of "onTap"-GestureDetector.

Goal: I want to make the red container clickable (e.g. GestureDetector). Problem: Red container does not react to onTap().

import "dart:developer" as dev;
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:sdg3/Widget/rotation_controller.dart';
import 'package:sdg3/Widget/rotation_menu_button.dart';
import 'package:vector_math/vector_math.dart' show radians, Vector3;

class RadialMenu extends StatefulWidget {
  final Widget open;
  final Widget? close;
  final int startAngle;
  final List<RotationMenuButton> rotationButtons;
  final RotationController? rotationController;

  const RadialMenu({
    Key? key,
    this.rotationController,
    required this.open,
    this.startAngle = 0,
    this.close,
    required this.rotationButtons,
  }) : super(key: key);

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

class _RadialMenuState extends State<RadialMenu>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late double maxButtonRadius;

  @override
  void initState() {
    super.initState();
    //get "biggest" button
    maxButtonRadius = widget.rotationButtons.map((e) => e.size).reduce(max);

    controller = AnimationController(
        duration: const Duration(milliseconds: 1500), vsync: this)
      ..addListener(() => setState(() {}));
    translation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(
      CurvedAnimation(parent: controller, curve: Curves.easeInOutCirc),
    );
    scale = Tween<double>(
      begin: 1.5,
      end: 0.0,
    ).animate(
      CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn),
    );
    rotation = Tween<double>(
      begin: 0.0,
      end: 360.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.0,
          0.7,
          curve: Curves.easeInOutCirc,
        ),
      ),
    );
  }

 @override
  Widget build(BuildContext context) {
    Widget open = widget.open;
    Widget? close = widget.close;
    var buttonsMap = widget.rotationButtons.asMap();
    List<Widget> buttons = buttonsMap.entries.map((e) {
      RotationMenuButton button = e.value;
      double singleangle = 360.0 / buttonsMap.length;
      double angle = (e.key * singleangle + widget.startAngle) % 360;
      return LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          double menuwidth = (min(constraints.maxWidth, constraints.maxHeight) -
                  maxButtonRadius) /
              2.0;
          //minimizie space between cicrles
          double dimensionsOfButton = 1 + button.size * (1 - scale.value / 1.5);

          double overlapRadiusOfButton =
              2.0 * sin(radians(singleangle / 2.0)) * menuwidth;
          if (dimensionsOfButton > overlapRadiusOfButton) {
            dimensionsOfButton = overlapRadiusOfButton - 5;
          }

          return Visibility(
            visible: scale.value < 1.0,
            child: Container(
              color: Colors.blue,
              child: Transform(
                transform: Matrix4.identity()
                  ..translate(
                      (translation.value * menuwidth) * cos(radians(angle)),
                      (translation.value * menuwidth) * sin(radians(angle))),
                child: ConstrainedBox(
                  constraints: BoxConstraints(
                      maxHeight: dimensionsOfButton,
                      maxWidth: dimensionsOfButton),
                  child: GestureDetector(
                      onTap: () => dev.log("I was pushed"),
                      child: Container(color: Colors.red)),
                ),
              ),
            ),
          );
        },
      );
    }).toList(growable: false);

    return AnimatedBuilder(
      animation: controller,
      builder: (context, widget) {
        return Transform.rotate(
          angle: radians(rotation.value),
          child: Stack(
            clipBehavior: Clip.none,
            alignment: Alignment.center,
            children: [
              if (close != null)
                Transform.scale(
                  scale: 1.5 - scale.value,
                  child: GestureDetector(
                    onTap: _close,
                    child: close,
                  ),
                ),
              Transform.scale(
                scale: scale.value,
                child: GestureDetector(
                  onTap: _open,
                  child: open,
                ),
              ),
              buttons.first
            ],
          ),
        );
      },
    );
  }

  late final Animation<double> rotation;
  late final Animation<double> translation;
  late final Animation<double> scale;

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

  _open() {
    controller.forward();
  }

  _close() {
    controller.reverse();
  }

The problem is the dev.log("I was pushed"). It never appears (also inkwell does not work neither does a GestureDetector.behaviour).

This is the output of the animation. The blue container ist just for visualisation where the Container is placed before Tranform, the red container is the correct "Button" after transform which does not respond to onTap():

When I remove the translation:

                  ..translate(
                      (translation.value * menuwidth) * cos(radians(angle)),
                      (translation.value * menuwidth) * sin(radians(angle))),

it works, however that is not the desired output.

I have the impression that hit-Tests are not tranlated correctly. Do you have an idea? Thanks!

enter image description here



Solution 1:[1]

I used a workaround using Positioned instead of translation. You´ll require a bit mathematics to repositioned it but it works with GestureDetector onTap() (here not shown, but its inside the button widget) including translation:

          return Visibility(
            visible: scale.value < 1.0,
            child: Stack(
              children: [
                Positioned(
                  left: constraints.maxWidth / 2 -
                      maxButtonRadius / 2 +
                      (translation.value * menuwidth) * cos(radians(angle)),
                  top: constraints.maxHeight / 2 -
                      maxButtonRadius / 2 +
                      (translation.value * menuwidth) * sin(radians(angle)),
                  child: SizedBox(
                    height: dimensionsOfButton,
                    width: dimensionsOfButton,
                    child: button,
                  ),
                ),
              ],
            ),
          );

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Defarine