前言
红包动画作用完成,如图:
该作用的完成莫非其实比较简单,便是根底的平移、旋转和缩放动画,但比较麻烦的便是需要写许多小动画组合,共由11个小动画组合而成。
动画拆解
红包显现动画
红包显现时的动画,由0到1的扩大。
late AnimationController controller;
late Animation<double> animation;
///红包打开动画
controller = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
controller.forward();
});
animation = Tween(begin: 0.0, end: 1.0).animate(controller);
红包未开前,平移动画
红包未开前,全体轻轻上下平移
late AnimationController bgController;
late Animation<Offset> bgAnimation;
///红包布景上下平移动画
bgController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
bgAnimation = Tween<Offset>(begin: Offset.zero, end: const Offset(0.0, -10))
.animate(bgController);
红包未开前,”开“按钮缩放动画
“开”按钮缩放动画,由1到0.8,动画循环履行。
late AnimationController openBtController;
late Animation<double> openBtAnimation;
///红包未开时,按钮缩放动画
openBtController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
openBtAnimation = Tween(begin: 1.0, end: 0.8).animate(openBtController);
红包敞开后,布景光显现动画
开红包后,会显现布景光
late AnimationController openLightScaleController;
late Animation<double> openLightScaleAnimation;
///开红包后,显现布景光
openLightScaleController = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
openLightScaleAnimation =
Tween(begin: 0.4, end: 1.0).animate(openLightScaleController);
红包敞开后,布景光扩大动画
开红包后,布景光轻轻扩大1.2倍,动画循环履行。
late AnimationController lightScaleController;
late Animation<double> lightScaleAnimation;
///布景光扩大动画,只扩大1.2倍
lightScaleController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
lightScaleAnimation =
Tween(begin: 1.0, end: 1.2).animate(lightScaleController);
红包敞开后,布景光旋转动画
开红包后,布景光轻轻旋转,只旋转0.02的弧度,动画循环履行。
late AnimationController lightRotateController;
late Animation<double> lightRotateAnimation;
///布景光旋转动画,轻轻旋转,只旋转0.02的弧度
lightRotateController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
lightRotateAnimation =
Tween(begin: 0.0, end: 0.02).animate(lightRotateController);
红包敞开后,新布景扩大动画
开红包后,原红包布景缩小至不见,新红包布景显现
late AnimationController openController;
late Animation<double> openAnimation;
///开红包 布景扩大动画
openController = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
openAnimation = Tween(begin: 0.4, end: 1.0).animate(openController);
红包敞开后,新布景平移动画
开红包后,新布景也轻轻上下平移
late AnimationController openBgController;
late Animation<Offset> openBgAnimation;
///红包布景上下平移动画
openBgController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
openBgAnimation =
Tween<Offset>(begin: Offset.zero, end: const Offset(0.0, -10))
.animate(openBgController);
红包敞开后,”当即使用“按钮缩放动画
开红包后,”当即使用“按钮缩放动画,由1到0.9,动画循环履行。
late AnimationController useBtController;
late Animation<double> useBtAnimation;
///当即使用按钮缩放动画
useBtController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
useBtAnimation = Tween(begin: 1.0, end: 0.9).animate(useBtController);
红包敞开后,金额显现的卡片上移动画
开红包后,显现金额的卡片上移
late final AnimationController offsetTopController;
late final Animation<Offset> offsetTopAnimation;
///卡片上移动画
offsetTopController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
offsetTopAnimation = Tween<Offset>(
begin: const Offset(0, 0.6),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: offsetTopController,
curve: Curves.easeInOutCubic,
));
红包敞开后,金额显现的动画
开红包后,金额会从某个数值自增至实践金额数值
late AnimationController priceController;
late Animation<double> priceAnimation;
///金额改换作用
price = widget.price;
double startPrice = 0;
if (price <= 100) {
///小于100的金额从0开始自增
startPrice = 0;
} else {
///大于100的金额对半开始自增
startPrice = price / 2;
}
priceController = AnimationController(
duration: const Duration(milliseconds: 600), vsync: this);
priceAnimation =
Tween(begin: startPrice, end: price).animate(priceController);
资源文件
class ImageAssets{
static const String homeIcRed1Webp = 'assets/home/ic-red-1.webp';
static const String homeIcRedBgWebp = 'assets/home/ic-red-bg.webp';
static const String homeIcRedLightWebp = 'assets/home/ic-red-light.webp';
static const String homeIcRedOpenWebp = 'assets/home/ic-red-open.webp';
static const String homeIcRed2BgWebp = 'assets/home/ic-red2-bg.webp';
static const String homeIcRed2BottomWebp = 'assets/home/ic-red2-bottom.webp';
static const String homeIcRed2BtWebp = 'assets/home/ic-red2-bt.webp';
static const String homeIcRed2TopBgWebp = 'assets/home/ic-red2-top-bg.webp';
static const String homeIcRed2TopWebp = 'assets/home/ic-red2-top.webp';
static const String homeIcCloseWhiteWebp = 'assets/home/ic-close-white.webp';
}
homeIcRedBgWebp :
homeIcRed1Webp:
homeIcRedLightWebp:
homeIcRedOpenWebp:
homeIcRed2BgWebp:
homeIcRed2BottomWebp:
homeIcRed2BtWebp:
homeIcRed2TopBgWebp:
homeIcRed2TopWebp:
完整代码
项目用到了GetX,需要留意导入。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
///新人红包
class RedEnvelopeDialog extends StatefulWidget {
double price;
RedEnvelopeDialog({Key? key, required this.price}) : super(key: key);
@override
_PageState createState() => _PageState();
}
class _PageState extends State<RedEnvelopeDialog>
with TickerProviderStateMixin {
double width = 0;
double height = 0;
double openSize = 0;
double btBgTopMargin = 0;
double openBgBottomMargin = 0;
double openBgTopMargin = 0;
double openTopBgHeight = 0;
double openBottomBgHeight = 0;
double openTopBgBottomMargin = 0;
double moveHeight = 0;
double openHeight = 0;
double useBtWidth = 0;
late AnimationController controller;
late Animation<double> animation;
late AnimationController openBtController;
late Animation<double> openBtAnimation;
late AnimationController lightScaleController;
late Animation<double> lightScaleAnimation;
late AnimationController lightRotateController;
late Animation<double> lightRotateAnimation;
late AnimationController openLightScaleController;
late Animation<double> openLightScaleAnimation;
late AnimationController bgController;
late Animation<Offset> bgAnimation;
late AnimationController openController;
late Animation<double> openAnimation;
late AnimationController openBgController;
late Animation<Offset> openBgAnimation;
late AnimationController useBtController;
late Animation<double> useBtAnimation;
late final AnimationController offsetTopController;
late final Animation<Offset> offsetTopAnimation;
late AnimationController priceController;
late Animation<double> priceAnimation;
RxBool showOpen = false.obs;
double price = 0;
@override
void initState() {
super.initState();
///依据设计稿比例核算实践数值
width = Get.width - 100;
height = (332 / 273) * width;
openSize = (78 / 332) * height;
btBgTopMargin = (20 / 332) * height;
openHeight = (332 / 273) * width;
openBgBottomMargin = (12 / 273) * width;
openBgTopMargin = (50 / 273) * width;
openTopBgHeight = (194 / 273) * width;
openBottomBgHeight = (189 / 273) * width;
openTopBgBottomMargin = (138 / 273) * width;
moveHeight = (143 / 273) * width;
useBtWidth = (178 / 273) * width;
///红包打开动画
controller = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
controller.forward();
});
animation = Tween(begin: 0.0, end: 1.0).animate(controller);
///开按钮缩放动画
openBtController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
openBtAnimation = Tween(begin: 1.0, end: 0.8).animate(openBtController);
///布景光显现动画
openLightScaleController = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
openLightScaleAnimation =
Tween(begin: 0.4, end: 1.0).animate(openLightScaleController);
///布景光扩大动画,只扩大1.2倍
lightScaleController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
lightScaleAnimation =
Tween(begin: 1.0, end: 1.2).animate(lightScaleController);
///布景光旋转动画,轻轻旋转,只旋转0.02的弧度
lightRotateController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
lightRotateAnimation =
Tween(begin: 0.0, end: 0.02).animate(lightRotateController);
///红包布景上下平移动画
bgController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
bgAnimation = Tween<Offset>(begin: Offset.zero, end: const Offset(0.0, -10))
.animate(bgController);
///开红包 布景扩大动画
openController = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
openAnimation = Tween(begin: 0.4, end: 1.0).animate(openController);
///开红包布景上下平移动画
openBgController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
openBgAnimation =
Tween<Offset>(begin: Offset.zero, end: const Offset(0.0, -10))
.animate(openBgController);
///开按钮缩放动画
useBtController = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this)
..repeat(reverse: true);
useBtAnimation = Tween(begin: 1.0, end: 0.9).animate(useBtController);
///卡片上移动画
offsetTopController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
offsetTopAnimation = Tween<Offset>(
begin: const Offset(0, 0.6),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: offsetTopController,
curve: Curves.easeInOutCubic,
));
///金额改换作用
price = widget.price;
double startPrice = 0;
if (price <= 100) {
///小于100的金额从0开始自增
startPrice = 0;
} else {
///大于100的金额对半开始自增
startPrice = price / 2;
}
priceController = AnimationController(
duration: const Duration(milliseconds: 600), vsync: this);
priceAnimation =
Tween(begin: startPrice, end: price).animate(priceController);
}
@override
void dispose() {
controller.dispose();
openBtController.dispose();
lightScaleController.dispose();
lightRotateController.dispose();
openLightScaleController.dispose();
bgController.dispose();
openController.dispose();
openBgController.dispose();
useBtController.dispose();
offsetTopController.dispose();
priceController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Stack(
alignment: Alignment.center,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
///布景光
Obx(
() => Visibility(
visible: showOpen.value,
child: AnimatedBuilder(
animation: openLightScaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: openLightScaleAnimation.value,
child: AnimatedBuilder(
animation: lightScaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: lightScaleAnimation.value,
child: AnimatedBuilder(
animation: lightRotateAnimation,
builder: (context, child) {
return Transform.rotate(
angle: lightRotateAnimation.value,
child: Image.asset(
ImageAssets.homeIcRedLightWebp,
width: double.infinity,
fit: BoxFit.fitWidth,
),
);
},
),
);
},
),
);
},
),
),
),
///开红包前
AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.scale(
scale: animation.value,
child: Container(
margin: EdgeInsets.all(50),
child: AnimatedBuilder(
animation: bgAnimation,
builder: (context, child) {
return Transform.translate(
offset: bgAnimation.value,
child: Stack(
children: [
Image.asset(
ImageAssets.homeIcRedBgWebp,
width: double.infinity,
fit: BoxFit.fitWidth,
),
SizedBox(
height: height,
width: width,
child: Column(
children: [
Expanded(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"新人见面礼",
style: TextStyle(
fontSize: 30,
color: const Color(
0xFFFFDC81)),
),
SizedBox(
height: 10,
),
Text(
"$priceRellyStr元",
style: TextStyle(
fontSize: 30,
color: const Color(
0xFFFFBB81)),
),
SizedBox(
height: 5,
),
Container(
padding:
EdgeInsets.symmetric(
horizontal: 12,
vertical: 4),
decoration: BoxDecoration(
color: const Color(
0xFFDC1215),
borderRadius:
BorderRadius
.circular(6),
),
child: Text(
"无门槛",
style: TextStyle(
fontSize: 15,
color: const Color(
0xFFFF862F)),
),
)
],
),
flex: 173,
),
Expanded(
child: Stack(
alignment:
Alignment.topCenter,
children: [
Container(
height: double.infinity,
alignment: Alignment
.bottomCenter,
margin: EdgeInsets.only(
bottom: 20),
child: Text(
"新人专享\u3000福利大派送",
style: TextStyle(
fontSize: 14,
color: const Color(
0xFFFF6571)),
),
),
Padding(
padding: EdgeInsets.only(
top: btBgTopMargin),
child: Image.asset(
ImageAssets
.homeIcRed1Webp,
width: double.infinity,
fit: BoxFit.fitWidth,
),
),
GestureDetector(
child: AnimatedBuilder(
animation:
openBtAnimation,
builder:
(context, child) {
return Transform
.scale(
scale:
openBtAnimation
.value,
child: Image.asset(
ImageAssets
.homeIcRedOpenWebp,
width: openSize,
height: openSize,
),
);
},
),
onTap: () {
controller.reverse();
showOpen.value = true;
openController
.forward();
openLightScaleController
.forward();
offsetTopController
.forward()
.whenComplete(() =>
offsetTopController
.stop());
priceController
.forward()
.whenComplete(() =>
priceController
.stop());
},
)
],
),
flex: 159,
)
],
),
)
],
),
);
},
),
));
}),
///开红包后
Obx(() => Visibility(
visible: showOpen.value,
child: AnimatedBuilder(
animation: openAnimation,
builder: (context, child) {
return Transform.scale(
scale: openAnimation.value,
child: AnimatedBuilder(
animation: openBgAnimation,
builder: (context, child) {
return Transform.translate(
offset: openBgAnimation.value,
child: Container(
height: openHeight,
margin: EdgeInsets.all(50),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Padding(
padding: EdgeInsets.only(
bottom:
openBgBottomMargin,
top: openBgTopMargin),
child: Image.asset(
ImageAssets
.homeIcRed2BgWebp,
width: double.infinity,
fit: BoxFit.fitWidth,
),
),
Padding(
padding: EdgeInsets.only(
bottom:
openTopBgBottomMargin),
child: SlideTransition(
position:
offsetTopAnimation,
child: Stack(
alignment:
Alignment.center,
children: [
Image.asset(
ImageAssets
.homeIcRed2TopBgWebp,
height:
openTopBgHeight,
fit:
BoxFit.fitWidth,
),
Container(
height:
openTopBgHeight,
width: double
.infinity,
alignment:
Alignment
.center,
child: Column(
mainAxisAlignment:
MainAxisAlignment
.center,
crossAxisAlignment:
CrossAxisAlignment
.center,
children: [
Expanded(
child:
Column(
children: [
Stack(
alignment:
Alignment.center,
children: [
Image.asset(
ImageAssets.homeIcRed2TopWebp,
height: (30 / 273) * width,
fit: BoxFit.fitHeight,
),
Container(
width: (184 / 273) * width,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text(
"开门红包",
style: TextStyle(
fontSize: 16,
color: const Color(0xFFBA683D),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
],
),
Expanded(
child:
Container(
alignment:
Alignment.center,
child:
Row(
crossAxisAlignment:
CrossAxisAlignment.end,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: priceController,
builder: (BuildContext context, Widget? child) {
return Text(
priceStr,
style: TextStyle(fontSize: 48, color: const Color(0xFFF30313), height: 1, fontWeight: FontWeight.bold),
);
},
),
Text(
'元',
style: TextStyle(fontSize: 20, height: 2, color: Color(0xFF141414)),
)
],
),
))
],
),
flex: 128,
),
Expanded(
child:
Column(
children: [
Expanded(
child:
Container(
alignment:
Alignment.bottomCenter,
child: Text("永久有用",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: const Color(0xFF8A8A8A),
height: 1,
)),
),
flex:
2,
),
Expanded(
child:
const SizedBox(),
flex:
3,
)
],
),
flex: 65,
)
],
))
],
)),
),
Image.asset(
ImageAssets
.homeIcRed2BottomWebp,
width: double.infinity,
fit: BoxFit.fitWidth,
),
Column(
children: [
Expanded(
child: const SizedBox(),
flex: 143,
),
Expanded(
child: Container(
alignment:
Alignment.center,
child: Column(
mainAxisAlignment:
MainAxisAlignment
.center,
children: [
SizedBox(
height: 20),
AnimatedBuilder(
animation:
useBtAnimation,
builder:
(context,
child) {
return Transform.scale(
scale: useBtAnimation.value,
child: GestureDetector(
onTap: () {
//todo 点击事件
},
child: Stack(
alignment: Alignment.center,
children: [
Image.asset(
ImageAssets.homeIcRed2BtWebp,
width: useBtWidth,
fit: BoxFit.fitWidth,
),
Text("当即领取", style: TextStyle(fontSize: 16, color: const Color(0xFFFFF0E1))),
],
)));
}),
SizedBox(
height: ((30 /
273) *
width),
),
Text(
"可在“我的-优惠券”中查看",
style: TextStyle(
fontSize:
12,
color: const Color(
0xFFFF6571)),
),
],
)),
flex: 189,
),
],
)
],
),
));
}));
})))
],
)
],
),
//封闭按钮
GestureDetector(
onTap: () {
Get.back();
},
child: Container(
margin: EdgeInsets.only(bottom: height * 1.5, left: width),
child: Image.asset(
ImageAssets.homeIcCloseWhiteWebp,
width: 21,
height: 21,
),
))
],
),
);
}
///金额数值变化
String get priceStr {
if (price % 1 == 0) {
return (priceAnimation.value.toInt()).toString();
} else {
return priceAnimation.value.toStringAsFixed(2);
}
}
///小数据点后没有尾数则不显现
String get priceRellyStr {
if (price % 1 == 0) {
return (price.toInt()).toString();
} else {
String pr = price.toStringAsFixed(2);
if (pr.endsWith("0")) {
return pr.substring(0, pr.length - 1);
}
return price.toStringAsFixed(2);
}
}
}