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(),
),
);
}
}
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:
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:
Check out this Gist I created for you as an example so you can see now that your icons become clickable.