我想在 Flutter 中实现以下动画。我已经为此使用 GestureDetector 创建了容器,但不知道如何实现动画。
步骤如下:
目前,如果任何一个项目被点击,它会登陆到没有动画的新页面
以下是我现在使用的代码。我需要像附加的 gif 一样的精确输出。
GIF:See in Google Drive(请在新标签页中打开,否则此页面将被导航)
代码:
double width = MediaQuery.of(context).size.width;
Container(
height: width * 0.6,
width: width * 0.6,
alignment: Alignment.center,
child: Row(
children: [
Column(
children: [
GestureDetector(
onTap: () {
Methods.navigationToDetailsPage(context, 'Category');
},
child: Container(
height: width * 0.6 * 0.5,
width: width * 0.6 * 0.5,
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(width * 0.06, width * 0.06, 2, 2),
child: Image.asset(
'assets/images/category.png',
),
),
),
GestureDetector(
onTap: () {
Methods.navigationToDetailsPage(context, 'Segment');
},
child: Container(
height: width * 0.6 * 0.5,
width: width * 0.6 * 0.5,
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(width * 0.06, 2, 2, width * 0.06),
child: Image.asset(
'assets/images/segment.png',
),
),
),
],
),
Column(
children: [
GestureDetector(
onTap: () {
Methods.navigationToDetailsPage(context, 'Division');
},
child: Container(
height: width * 0.6 * 0.5,
width: width * 0.6 * 0.5,
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(2, width * 0.06, width * 0.06, 2),
child: Image.asset(
'assets/images/division.png',
),
),
),
GestureDetector(
onTap: () {
Methods.navigationToDetailsPage(context, 'Brand');
},
child: Container(
height: width * 0.6 * 0.5,
width: width * 0.6 * 0.5,
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(2, 2, width * 0.06, width * 0.06),
child: Image.asset(
'assets/images/brand.png',
),
),
),
],
),
],
),
)
答案 0 :(得分:0)
AnimationController
,请参阅创建的 AnimatedSectorButton
小部件。Tween
动画使用 Curves
来平滑动画流,请参阅创建的 SectorTile
小部件。ClipPath
和 CustomClipper
完成的。通常应避免使用带有文本的图像进行布局,因为它们的可定制性较差,并且它们使 f.ex 变得更加困难。翻译每个资产给定的应用程序,您需要每种语言的图像。最好是潜在地导入字体并自己创建一个漂亮的小部件。
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
main() {
runApp(StackOverflowExampleApp());
}
class StackOverflowExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSectorButton(radius: 150, sectorQuadrant: SectorQuadrant.TopLeft),
AnimatedSectorButton(radius: 150, sectorQuadrant: SectorQuadrant.TopRight),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSectorButton(radius: 150, sectorQuadrant: SectorQuadrant.BottomLeft),
AnimatedSectorButton(radius: 150, sectorQuadrant: SectorQuadrant.BottomRight),
],
)
]),
),
),
);
}
}
class AnimatedSectorButton extends StatefulWidget {
final double radius;
final SectorQuadrant sectorQuadrant;
const AnimatedSectorButton({required this.radius, required this.sectorQuadrant});
_AnimatedSectorButtonState createState() => _AnimatedSectorButtonState();
}
class _AnimatedSectorButtonState extends State<AnimatedSectorButton> with TickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: const Duration(milliseconds: 1000), vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(Duration(milliseconds: 1500)).then((value) async {
_controller.reverse();
});
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return SectorTile(
radius: widget.radius,
quadrant: widget.sectorQuadrant,
controller: _controller.view,
onTap: () {
_controller.forward();
},
);
}
}
enum SectorQuadrant {
TopRight,
TopLeft,
BottomLeft,
BottomRight
}
class SectorTile extends StatelessWidget {
final double radius;
final SectorQuadrant quadrant;
final Animation<double> controller;
final Function() onTap;
late final Animation<double> offsetValue;
late final double xSign;
late final double ySign;
SectorTile({
Key? key,
required this.radius,
required this.quadrant,
required this.controller,
required this.onTap,
}) : super(key: key) {
// Here we define the specific of the animation.
offsetValue = Tween(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.fastOutSlowIn))
.animate(controller);
// Primarily used for the direction of the animation
xSign = quadrant == SectorQuadrant.TopLeft || quadrant == SectorQuadrant.BottomLeft ? -1 : 1;
ySign = quadrant == SectorQuadrant.TopLeft || quadrant == SectorQuadrant.TopRight ? -1 : 1;
}
Widget _buildAnimation(BuildContext context, Widget? widget) {
double value = offsetValue.value * (radius / 3);
return Transform.translate(
offset: Offset(value * xSign, value * ySign),
child: ClipPath(
clipper: SectorClipper(quadrant),
child: Material(
color: Colors.red,
child: InkWell(
splashColor: Colors.black87,
onTap: onTap,
child: Container(
width: radius,
height: radius,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Align(
alignment: Alignment(-xSign, -ySign),
child: Text(
"StackOverflow",
style: TextStyle(color: Colors.white),
),
),
),
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(animation: controller, builder: _buildAnimation);
}
}
// Needed for constraining the material splash effect.
class SectorClipper extends CustomClipper<Path> {
final SectorQuadrant sectorQuadrant;
SectorClipper(this.sectorQuadrant);
@override
Path getClip(Size size) {
switch (sectorQuadrant) {
case SectorQuadrant.TopRight:
return Path()..addOval(Rect.fromCircle(center: Offset(0, size.height), radius: size.height));
case SectorQuadrant.TopLeft:
return Path()..addOval(Rect.fromCircle(center: Offset(size.width, size.height), radius: size.height));
case SectorQuadrant.BottomLeft:
return Path()..addOval(Rect.fromCircle(center: Offset(size.width, 0), radius: size.height));
case SectorQuadrant.BottomRight:
return Path()..addOval(Rect.fromCircle(center: Offset(0, 0), radius: size.height));
}
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => true;
}