如何将手势功能添加到已在 Flutter 中设置动画的小部件?
How do you add gesture functionality to a widget that has been animated in Flutter?
我在 Flutter 中围绕 select个人资料图片创建了一个动画。当用户单击他们的个人资料图片(或 'Add Photo' 占位符)时,会弹出两个按钮,其中包含拍照选项或 select 他们图库中的照片(提供了说明性屏幕截图)
我的问题是两个动画按钮上的手势检测似乎不起作用。添加一个
我删除了一些杂乱的内容并粘贴了下面代码的相关部分。任何人都可以看到我哪里出错了吗?
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(),
),
);
}
}
您的问题是包裹包含您的按钮的 Stack 的 Positioned 小部件正在剪裁可点击区域;您的按钮正在工作 - 它们只是被您通过 Postioned 小部件指定给它们的区域遮挡了。
你是这样的:
Stack(
children: [
Positioned(
top: 30,
left: (size.width) / 2 - 70,
// YOU DON'T HAVE ANY RIGHT POSITIONING HERE
child: Stack(
children: [
AnimatedBuilder(),
AnimatedBuilder(),
GestureDetector()
]
)
]
)
这使您的可点击区域变成了这样:
您至少需要将 Positioned 小部件的正确位置设置为 0,如:
Stack(
children: [
Positioned(
top: 30,
left: (size.width) / 2 - 70,
right: 0,
child: Stack(
children: [
AnimatedBuilder(),
AnimatedBuilder(),
GestureDetector()
]
)
]
)
这使您的可点击区域变成了这样:
查看我为您创建的这个 Gist 作为示例,这样您现在就可以看到您的图标变得可以点击了。
我在 Flutter 中围绕 select个人资料图片创建了一个动画。当用户单击他们的个人资料图片(或 'Add Photo' 占位符)时,会弹出两个按钮,其中包含拍照选项或 select 他们图库中的照片(提供了说明性屏幕截图)
我的问题是两个动画按钮上的手势检测似乎不起作用。添加一个
我删除了一些杂乱的内容并粘贴了下面代码的相关部分。任何人都可以看到我哪里出错了吗?
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(),
),
);
}
}
您的问题是包裹包含您的按钮的 Stack 的 Positioned 小部件正在剪裁可点击区域;您的按钮正在工作 - 它们只是被您通过 Postioned 小部件指定给它们的区域遮挡了。
你是这样的:
Stack(
children: [
Positioned(
top: 30,
left: (size.width) / 2 - 70,
// YOU DON'T HAVE ANY RIGHT POSITIONING HERE
child: Stack(
children: [
AnimatedBuilder(),
AnimatedBuilder(),
GestureDetector()
]
)
]
)
这使您的可点击区域变成了这样:
您至少需要将 Positioned 小部件的正确位置设置为 0,如:
Stack(
children: [
Positioned(
top: 30,
left: (size.width) / 2 - 70,
right: 0,
child: Stack(
children: [
AnimatedBuilder(),
AnimatedBuilder(),
GestureDetector()
]
)
]
)
这使您的可点击区域变成了这样:
查看我为您创建的这个 Gist 作为示例,这样您现在就可以看到您的图标变得可以点击了。