Flutter實(shí)現(xiàn)紅包動(dòng)畫(huà)效果的示例代碼
前言
紅包動(dòng)畫(huà)效果實(shí)現(xiàn),如圖:
該效果的實(shí)現(xiàn)難道其實(shí)比較簡(jiǎn)單,就是基礎(chǔ)的平移、旋轉(zhuǎn)和縮放動(dòng)畫(huà),但比較麻煩的就是需要寫(xiě)很多小動(dòng)畫(huà)組合,共由11個(gè)小動(dòng)畫(huà)組合而成。
動(dòng)畫(huà)拆解
紅包顯示動(dòng)畫(huà)
紅包顯示時(shí)的動(dòng)畫(huà),由0到1的放大。
late AnimationController controller; late Animation<double> animation; ///紅包展開(kāi)動(dòng)畫(huà) 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);
紅包未開(kāi)前,平移動(dòng)畫(huà)
紅包未開(kāi)前,整體微微上下平移
late AnimationController bgController; late Animation<Offset> bgAnimation; ///紅包背景上下平移動(dòng)畫(huà) 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);
紅包未開(kāi)前,”開(kāi)“按鈕縮放動(dòng)畫(huà)
"開(kāi)"按鈕縮放動(dòng)畫(huà),由1到0.8,動(dòng)畫(huà)循環(huán)執(zhí)行。
late AnimationController openBtController; late Animation<double> openBtAnimation; ///紅包未開(kāi)時(shí),按鈕縮放動(dòng)畫(huà) openBtController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); openBtAnimation = Tween(begin: 1.0, end: 0.8).animate(openBtController);
紅包開(kāi)啟后,背景光顯示動(dòng)畫(huà)
開(kāi)紅包后,會(huì)顯示背景光
late AnimationController openLightScaleController; late Animation<double> openLightScaleAnimation; ///開(kāi)紅包后,顯示背景光 openLightScaleController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this); openLightScaleAnimation = Tween(begin: 0.4, end: 1.0).animate(openLightScaleController);
紅包開(kāi)啟后,背景光放大動(dòng)畫(huà)
開(kāi)紅包后,背景光微微放大1.2倍,動(dòng)畫(huà)循環(huán)執(zhí)行。
late AnimationController lightScaleController; late Animation<double> lightScaleAnimation; ///背景光放大動(dòng)畫(huà),只放大1.2倍 lightScaleController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); lightScaleAnimation = Tween(begin: 1.0, end: 1.2).animate(lightScaleController);
紅包開(kāi)啟后,背景光旋轉(zhuǎn)動(dòng)畫(huà)
開(kāi)紅包后,背景光微微旋轉(zhuǎn),只旋轉(zhuǎn)0.02的弧度,動(dòng)畫(huà)循環(huán)執(zhí)行。
late AnimationController lightRotateController; late Animation<double> lightRotateAnimation; ///背景光旋轉(zhuǎn)動(dòng)畫(huà),微微旋轉(zhuǎn),只旋轉(zhuǎn)0.02的弧度 lightRotateController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); lightRotateAnimation = Tween(begin: 0.0, end: 0.02).animate(lightRotateController);
紅包開(kāi)啟后,新背景放大動(dòng)畫(huà)
開(kāi)紅包后,原紅包背景縮小至不見(jiàn),新紅包背景顯示
late AnimationController openController; late Animation<double> openAnimation; ///開(kāi)紅包 背景放大動(dòng)畫(huà) openController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this); openAnimation = Tween(begin: 0.4, end: 1.0).animate(openController);
紅包開(kāi)啟后,新背景平移動(dòng)畫(huà)
開(kāi)紅包后,新背景也微微上下平移
late AnimationController openBgController; late Animation<Offset> openBgAnimation; ///紅包背景上下平移動(dòng)畫(huà) 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);
紅包開(kāi)啟后,”立即使用“按鈕縮放動(dòng)畫(huà)
開(kāi)紅包后,”立即使用“按鈕縮放動(dòng)畫(huà),由1到0.9,動(dòng)畫(huà)循環(huán)執(zhí)行。
late AnimationController useBtController; late Animation<double> useBtAnimation; ///立即使用按鈕縮放動(dòng)畫(huà) useBtController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); useBtAnimation = Tween(begin: 1.0, end: 0.9).animate(useBtController);
紅包開(kāi)啟后,金額顯示的卡片上移動(dòng)畫(huà)
開(kāi)紅包后,顯示金額的卡片上移
late final AnimationController offsetTopController; late final Animation<Offset> offsetTopAnimation; ///卡片上移動(dòng)畫(huà) 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, ));
紅包開(kāi)啟后,金額顯示的動(dòng)畫(huà)
開(kāi)紅包后,金額會(huì)從某個(gè)數(shù)值自增至實(shí)際金額數(shù)值
late AnimationController priceController; late Animation<double> priceAnimation; ///金額變換效果 price = widget.price; double startPrice = 0; if (price <= 100) { ///小于100的金額從0開(kāi)始自增 startPrice = 0; } else { ///大于100的金額對(duì)半開(kāi)始自增 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:
完整代碼
項(xiàng)目用到了GetX,需要注意導(dǎo)入。
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(); ///根據(jù)設(shè)計(jì)稿比例計(jì)算實(shí)際數(shù)值 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; ///紅包展開(kāi)動(dòng)畫(huà) 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); ///開(kāi)按鈕縮放動(dòng)畫(huà) openBtController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); openBtAnimation = Tween(begin: 1.0, end: 0.8).animate(openBtController); ///背景光顯示動(dòng)畫(huà) openLightScaleController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this); openLightScaleAnimation = Tween(begin: 0.4, end: 1.0).animate(openLightScaleController); ///背景光放大動(dòng)畫(huà),只放大1.2倍 lightScaleController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); lightScaleAnimation = Tween(begin: 1.0, end: 1.2).animate(lightScaleController); ///背景光旋轉(zhuǎn)動(dòng)畫(huà),微微旋轉(zhuǎn),只旋轉(zhuǎn)0.02的弧度 lightRotateController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); lightRotateAnimation = Tween(begin: 0.0, end: 0.02).animate(lightRotateController); ///紅包背景上下平移動(dòng)畫(huà) 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); ///開(kāi)紅包 背景放大動(dòng)畫(huà) openController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this); openAnimation = Tween(begin: 0.4, end: 1.0).animate(openController); ///開(kāi)紅包背景上下平移動(dòng)畫(huà) 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); ///開(kāi)按鈕縮放動(dòng)畫(huà) useBtController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this) ..repeat(reverse: true); useBtAnimation = Tween(begin: 1.0, end: 0.9).animate(useBtController); ///卡片上移動(dòng)畫(huà) 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開(kāi)始自增 startPrice = 0; } else { ///大于100的金額對(duì)半開(kāi)始自增 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, ), ); }, ), ); }, ), ); }, ), ), ), ///開(kāi)紅包前 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( "新人見(jiàn)面禮", 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( "無(wú)門(mén)檻", 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( "新人專(zhuān)享\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, ) ], ), ) ], ), ); }, ), )); }), ///開(kāi)紅包后 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( "開(kāi)門(mén)紅包", 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 點(diǎn)擊事件 }, child: Stack( alignment: Alignment.center, children: [ Image.asset( ImageAssets.homeIcRed2BtWebp, width: useBtWidth, fit: BoxFit.fitWidth, ), Text("立即領(lǐng)取", style: TextStyle(fontSize: 16, color: const Color(0xFFFFF0E1))), ], ))); }), SizedBox( height: ((30 / 273) * width), ), Text( "可在“我的-優(yōu)惠券”中查看", style: TextStyle( fontSize: 12, color: const Color( 0xFFFF6571)), ), ], )), flex: 189, ), ], ) ], ), )); })); }))) ], ) ], ), //關(guān)閉按鈕 GestureDetector( onTap: () { Get.back(); }, child: Container( margin: EdgeInsets.only(bottom: height * 1.5, left: width), child: Image.asset( ImageAssets.homeIcCloseWhiteWebp, width: 21, height: 21, ), )) ], ), ); } ///金額數(shù)值變化 String get priceStr { if (price % 1 == 0) { return (priceAnimation.value.toInt()).toString(); } else { return priceAnimation.value.toStringAsFixed(2); } } ///小數(shù)據(jù)點(diǎn)后沒(méi)有尾數(shù)則不顯示 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); } } }
以上就是Flutter實(shí)現(xiàn)紅包動(dòng)畫(huà)效果的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Flutter紅包動(dòng)畫(huà)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android ViewPager制作新手導(dǎo)航頁(yè)(動(dòng)態(tài)加載)
這篇文章主要為大家詳細(xì)介紹了Android ViewPager制作新手導(dǎo)航頁(yè),了解什么是動(dòng)態(tài)加載指示器,感興趣的小伙伴們可以參考一下2016-05-05Android Studio下添加assets目錄的實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇Android Studio下添加assets目錄的實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03Android中實(shí)現(xiàn)ProgressBar菊花旋轉(zhuǎn)進(jìn)度條的動(dòng)畫(huà)效果
大家在一些頁(yè)面經(jīng)常會(huì)遇到加載中需要顯示一個(gè)加載動(dòng)畫(huà),像旋轉(zhuǎn)的菊花旋轉(zhuǎn)的圈圈動(dòng)畫(huà)效果,本文通過(guò)實(shí)例代碼給大家講解下,需要的朋友參考下吧2021-09-09Android實(shí)現(xiàn)短信發(fā)送功能
這篇文章主要介紹了Android實(shí)現(xiàn)短信發(fā)送功能,對(duì)Android實(shí)現(xiàn)短信發(fā)送的每一步都進(jìn)行了詳細(xì)的介紹,感興趣的小伙伴們可以參考一下2015-12-12Android編程實(shí)現(xiàn)兩個(gè)Activity之間共享數(shù)據(jù)及互相訪問(wèn)的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)兩個(gè)Activity之間共享數(shù)據(jù)及互相訪問(wèn)的方法,簡(jiǎn)單分析了Android中Activity數(shù)據(jù)共享與訪問(wèn)的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Android自定義View實(shí)現(xiàn)投票進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)投票進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11安卓(Android)開(kāi)發(fā)之分享帶文字的圖片
用過(guò)微信分享SDK的都應(yīng)該知道,微信分享到朋友圈的時(shí)候是不能同時(shí)分享圖片和文字的,只要有縮略圖,那么文字就不會(huì)生效。那么問(wèn)題就來(lái)了,如果我們想把APP內(nèi)的某些內(nèi)容連帶圖片一起分享到微信,是不是沒(méi)辦法了呢?下面一起來(lái)看看怎么解決。2016-08-08