'How do you add gesture functionality to a widget that has been animated in Flutter?

I have created an animation in Flutter around selection a profile picture. When the user clicks their profile picture (or 'Add Photo' placeholder), two buttons fly out with an option to either take a photo, or select a picture from their gallery (illustrative screenshot provided)

My problem is that gesture detection on the two buttons that are animated does not seem to work. Adding a

I've removed some clutter and have pasted the relevant portions of the code below.. Can anyone see where i'm going wrong?

  import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../providers/user_deets_provider.dart';
import '../widgets/text_input.dart';
import 'package:image_picker/image_picker.dart';

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

  @override
  State<UserDetails> createState() => _UserDetailsState();
}

class _UserDetailsState extends State<UserDetails>
    with SingleTickerProviderStateMixin {
  File? image;
  late AnimationController animationController;
  late Animation cameraTranslationAnimation, galleryTranslationAnimation;
  late Animation rotationAnimation;

  double getRadiansFromDegree(double degree) {
    double unitRadian = 57.295779513;
    return degree / unitRadian;
  }

  Future pickImage() async {
    try {
      final image = await ImagePicker().pickImage(source: ImageSource.gallery);
      if (image == null) return;

      final imageTemporary = File(image.path);
      setState(() {
        this.image = imageTemporary;
      });
    } on PlatformException catch (e) {
      print('Failed to pick image $e');
    }
  }

  @override
  void initState() {
    animationController = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 250));
    cameraTranslationAnimation = TweenSequence([
      TweenSequenceItem<double>(
          tween: Tween(begin: 0.0, end: 1.2), weight: 75.0),
      TweenSequenceItem<double>(
          tween: Tween(begin: 1.2, end: 1.0), weight: 25.0)
    ]).animate(animationController);
    galleryTranslationAnimation = TweenSequence([
      TweenSequenceItem<double>(
          tween: Tween(begin: 0.0, end: 1.4), weight: 55.0),
      TweenSequenceItem<double>(
          tween: Tween(begin: 1.4, end: 1.0), weight: 45.0)
    ]).animate(animationController);
    rotationAnimation = Tween<double>(begin: 180.0, end: 0.0).animate(
        CurvedAnimation(parent: animationController, curve: Curves.easeOut));
    super.initState();
    // animationController.addListener(() {
    //   setState(() {});
    // });
  }

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

  @override
  Widget build(BuildContext context) {
    TextEditingController nameController = TextEditingController(
        text: Provider.of<UserDeetsProvider>(context).name);
    TextEditingController jobTitleController = TextEditingController(
        text: Provider.of<UserDeetsProvider>(context).jobTitle);
    TextEditingController companyController = TextEditingController(
        text: Provider.of<UserDeetsProvider>(context).company);
    TextEditingController emailController = TextEditingController(
        text: Provider.of<UserDeetsProvider>(context).email);
    TextEditingController numberController = TextEditingController(
        text: Provider.of<UserDeetsProvider>(context).number);
    TextEditingController locationController = TextEditingController(
        text: Provider.of<UserDeetsProvider>(context).location);
    TextEditingController websiteController = TextEditingController(
        text: Provider.of<UserDeetsProvider>(context).website);
    Size size = MediaQuery.of(context).size;
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Details'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            SizedBox(
              width: size.width,
              height: 190,
              child: Stack(
                children: [
                  Positioned(
                    top: 30,
                    left: (size.width) / 2 - 70,
                    child: Stack(
                      children: [
                        AnimatedBuilder(
                          animation: animationController,
                          builder: (context, child) {
                            return Transform.translate(
                              offset: Offset.fromDirection(
                                  getRadiansFromDegree(30),
                                  cameraTranslationAnimation.value * 165),
                              child: Transform(
                                transform: Matrix4.rotationZ(
                                    getRadiansFromDegree(
                                        rotationAnimation.value))
                                  ..scale(cameraTranslationAnimation.value),
                                alignment: Alignment.center,
                                child: Container(
                                  decoration: const BoxDecoration(
                                      color: Colors.blue,
                                      shape: BoxShape.circle),
                                  width: 50,
                                  height: 50,
                                  child: IconButton(
                                    icon: const Icon(Icons.add_to_photos,
                                        color: Colors.white),
                                    onPressed: () {
                                      print('pressed');
                                    },
                                  ),
                                ),
                              ),
                            );
                          },
                        ),
                        AnimatedBuilder(
                          animation: animationController,
                          builder: (_, child) {
                            return Positioned(
                              left: 10,
                              child: Transform.translate(
                                offset: Offset.fromDirection(
                                    getRadiansFromDegree(365),
                                    galleryTranslationAnimation.value * 135),
                                child: Transform(
                                  transform: Matrix4.rotationZ(
                                      getRadiansFromDegree(
                                          rotationAnimation.value))
                                    ..scale(galleryTranslationAnimation.value),
                                  alignment: Alignment.center,
                                  child: CircularButton(
                                      width: 50,
                                      height: 50,
                                      color: Colors.white,
                                      icon: const Icon(Icons.camera_alt,
                                          color: Colors.black87),
                                      onClick: () {}),
                                ),
                              ),
                            );
                          },
                        ),
                        GestureDetector(
                          onTap: () {
                            if (animationController.isCompleted) {
                              animationController.reverse();
                            } else {
                              animationController.forward();
                            }
                          },
                          child: image != null
                              ? CircleAvatar(
                                  radius: 70,
                                  child: Image.file(image!),
                                )
                              : const CircleAvatar(
                                  radius: 70,
                                  child: Text('Add Photo'),
                                ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 20),
              child: Column(
                children: [
                  TextButton(
                      onPressed: () {
                        print('pressed');
                        pickImage();
                      },
                      child: Text('pick photo')),
                  DeetsTextInput(
                      controller: nameController,
                      label: 'Name',
                      callback: () =>
                          Provider.of<UserDeetsProvider>(context, listen: false)
                              .changeName(nameController.text)),
                  DeetsTextInput(
                      controller: jobTitleController,
                      label: 'Job Title',
                      callback: () =>
                          Provider.of<UserDeetsProvider>(context, listen: false)
                              .changeJob(jobTitleController.text)),
                  DeetsTextInput(
                      controller: companyController,
                      label: 'Company',
                      callback: () =>
                          Provider.of<UserDeetsProvider>(context, listen: false)
                              .changeCompany(companyController.text)),
                  DeetsTextInput(
                      controller: emailController,
                      label: 'Email',
                      callback: () =>
                          Provider.of<UserDeetsProvider>(context, listen: false)
                              .changeEmail(emailController.text)),
                  DeetsTextInput(
                      controller: numberController,
                      label: 'Number',
                      callback: () =>
                          Provider.of<UserDeetsProvider>(context, listen: false)
                              .changePhone(numberController.text)),
                  DeetsTextInput(
                      controller: locationController,
                      label: 'Location',
                      callback: () =>
                          Provider.of<UserDeetsProvider>(context, listen: false)
                              .changeLocation(locationController.text)),
                  DeetsTextInput(
                      controller: websiteController,
                      label: 'Website',
                      callback: () =>
                          Provider.of<UserDeetsProvider>(context, listen: false)
                              .changeWebsite(websiteController.text)),
                  const SizedBox(height: 30),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class CircularButton extends StatelessWidget {
  final double width;
  final double height;
  final Color color;
  final Icon icon;
  final VoidCallback onClick;

  const CircularButton(
      {Key? key,
      required this.width,
      required this.height,
      required this.color,
      required this.icon,
      required this.onClick})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(color: color, shape: BoxShape.circle),
      width: width,
      height: height,
      child: IconButton(
        icon: icon,
        onPressed: () => onClick(),
      ),
    );
  }
}

enter image description here



Solution 1:[1]

Your issue is that the Positioned widget that is wrapping the Stack that contains your buttons is clipping the clickable area; your buttons are working - they are just being occluded by the area you've designated to them by the Postioned widget.

You have it like this:


   Stack(
     children: [
        Positioned(
          top: 30,
          left: (size.width) / 2 - 70,
          // YOU DON'T HAVE ANY RIGHT POSITIONING HERE
          child: Stack(
            children: [
              AnimatedBuilder(),
              AnimatedBuilder(),
              GestureDetector()
            ]
        )
    ]
  )

Which makes your clickable area this:

enter image description here

You need to, at a minimum, set the right position of the Positioned widget to 0, as in:


   Stack(
     children: [
        Positioned(
          top: 30,
          left: (size.width) / 2 - 70,
          right: 0,
          child: Stack(
            children: [
              AnimatedBuilder(),
              AnimatedBuilder(),
              GestureDetector()
            ]
        )
    ]
  )

Which makes your clickable area this:

enter image description here

Check out this Gist I created for you as an example so you can see now that your icons become clickable.

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