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

Time:03-31

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

CodePudding user response:

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.

  • Related