Flutter實(shí)現(xiàn)牛頓擺動(dòng)畫效果的示例代碼
前言
牛頓擺大家應(yīng)該都不陌生,也叫碰碰球、永動(dòng)球(理論情況下),那么今天我們用Flutter實(shí)現(xiàn)這么一個(gè)理論中的永動(dòng)球,可以作為加載Loading使用。
- 知識點(diǎn):繪制、動(dòng)畫曲線、多動(dòng)畫狀態(tài)更新
效果圖:
實(shí)現(xiàn)步驟
1、繪制靜態(tài)效果
首先我們需要把線和小圓球繪制出來,對于看過我之前文章的小伙伴來說這個(gè)就很簡單了,效果圖:
關(guān)鍵代碼:
// 小圓球半徑 double radius = 6; /// 小球圓心和直線終點(diǎn)一致 //左邊小球圓心 Offset offset = Offset(20, 60); //右邊小球圓心 Offset offset2 = Offset(20 * 6 * 8, 60); Paint paint = Paint() ..color = Colors.black87 ..strokeWidth = 2; /// 繪制線 canvas.drawLine(Offset.zero, Offset(90, 0), paint); canvas.drawLine(Offset(20, 0), offset, paint); canvas.drawLine( Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint); canvas.drawLine( Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint); canvas.drawLine( Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint); canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint); /// 繪制小圓球 canvas.drawCircle(offset, radius, paint); canvas.drawCircle(Offset(20 + radius * 2, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint); canvas.drawCircle(offset2, radius, paint);
2、加入動(dòng)畫
思路: 我們可以看到5個(gè)小球一共2個(gè)小球在運(yùn)動(dòng),左邊小球運(yùn)動(dòng)一個(gè)來回之后傳遞給右邊小球,右邊小球開始運(yùn)動(dòng),右邊一個(gè)來回再傳遞給左邊開始,也就是左邊運(yùn)動(dòng)周期是:0-1-0,正向運(yùn)動(dòng)一次,反向再運(yùn)動(dòng)一次,這樣就是一個(gè)周期,右邊也是一樣,左邊運(yùn)動(dòng)完傳遞給右邊,右邊運(yùn)動(dòng)完傳遞給左邊,這樣就簡單實(shí)現(xiàn)了牛頓擺的效果。
兩個(gè)關(guān)鍵點(diǎn)
小球運(yùn)動(dòng)路徑: 小球的運(yùn)動(dòng)路徑是一個(gè)弧度,以豎線的起點(diǎn)為圓心,終點(diǎn)為半徑,那么我們只需要設(shè)置小球運(yùn)動(dòng)至最高點(diǎn)的角度即可,通過角度就可計(jì)算出小球的坐標(biāo)點(diǎn)。
運(yùn)動(dòng)曲線: 當(dāng)然我們知道牛頓擺小球的運(yùn)動(dòng)曲線并不是勻速的,他是有一個(gè)加速減速過程的,撞擊之后,小球先加速然后減速達(dá)到最高點(diǎn)速度為0,之后速度再從0慢慢加速進(jìn)行撞擊小球,周而復(fù)始。
下面的運(yùn)動(dòng)曲線就是先加速再減速,大概符合牛頓擺的運(yùn)動(dòng)曲線。我們就使用這個(gè)曲線看看效果。
完整源碼
class OvalLoading extends StatefulWidget { const OvalLoading({Key? key}) : super(key: key); @override _OvalLoadingState createState() => _OvalLoadingState(); } class _OvalLoadingState extends State<OvalLoading> with TickerProviderStateMixin { // 左邊小球 late AnimationController _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300)) ..addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); //反向執(zhí)行 1-0 } else if (status == AnimationStatus.dismissed) { _controller2.forward(); } }) ..forward(); // 右邊小球 late AnimationController _controller2 = AnimationController(vsync: this, duration: Duration(milliseconds: 300)) ..addStatusListener((status) { // dismissed 動(dòng)畫在起始點(diǎn)停止 // forward 動(dòng)畫正在正向執(zhí)行 // reverse 動(dòng)畫正在反向執(zhí)行 // completed 動(dòng)畫在終點(diǎn)停止 if (status == AnimationStatus.completed) { _controller2.reverse(); //反向執(zhí)行 1-0 } else if (status == AnimationStatus.dismissed) { // 反向執(zhí)行完畢左邊小球執(zhí)行 _controller.forward(); } }); late var cure = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic); late var cure2 = CurvedAnimation(parent: _controller2, curve: Curves.easeOutCubic); late Animation<double> animation = Tween(begin: 0.0, end: 1.0).animate(cure); late Animation<double> animation2 = Tween(begin: 0.0, end: 1.0).animate(cure2); @override Widget build(BuildContext context) { return Container( margin: EdgeInsetsDirectional.only(top: 300, start: 150), child: CustomPaint( size: Size(100, 100), painter: _OvalLoadingPainter( animation, animation2, Listenable.merge([animation, animation2])), ), ); } @override void dispose() { _controller.dispose(); _controller2.dispose(); super.dispose(); } } class _OvalLoadingPainter extends CustomPainter { double radius = 6; final Animation<double> animation; final Animation<double> animation2; final Listenable listenable; late Offset offset; // 左邊小球圓心 late Offset offset2; // 右邊小球圓心 final double lineLength = 60; // 線長 _OvalLoadingPainter(this.animation, this.animation2, this.listenable) : super(repaint: listenable) { offset = Offset(20, lineLength); offset2 = Offset(20 * radius * 8, lineLength); } // 擺動(dòng)角度 double angle = pi / 180 * 30; // 30° @override void paint(Canvas canvas, Size size) { Paint paint = Paint() ..color = Colors.black87 ..strokeWidth = 2; // 左邊小球 默認(rèn)坐標(biāo) 下方是90度 需要+pi/2 var dx = 20 + 60 * cos(pi / 2 + angle * animation.value); var dy = 60 * sin(pi / 2 + angle * animation.value); // 右邊小球 var dx2 = 20 + radius * 8 - 60 * cos(pi / 2 + angle * animation2.value); var dy2 = 60 * sin(pi / 2 + angle * animation2.value); offset = Offset(dx, dy); offset2 = Offset(dx2, dy2); /// 繪制線 canvas.drawLine(Offset.zero, Offset(90, 0), paint); canvas.drawLine(Offset(20, 0), offset, paint); canvas.drawLine( Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint); canvas.drawLine( Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint); canvas.drawLine( Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint); canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint); /// 繪制球 canvas.drawCircle(offset, radius, paint); canvas.drawCircle( Offset(20 + radius * 2, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint); canvas.drawCircle(offset2, radius, paint); } @override bool shouldRepaint(covariant _OvalLoadingPainter oldDelegate) { return oldDelegate.listenable != listenable; } }
去掉線的效果
總結(jié)
本文展示了實(shí)現(xiàn)牛頓擺的原理,其實(shí)并不復(fù)雜,關(guān)鍵點(diǎn)就是小球的運(yùn)動(dòng)軌跡和運(yùn)動(dòng)速度曲線,如果用到項(xiàng)目中當(dāng)做Loading還有很多優(yōu)化的空間,比如加上小球影子、修改小球顏色或者把小球換成好玩的圖片等等操作會看起來更好看一點(diǎn)
到此這篇關(guān)于Flutter實(shí)現(xiàn)牛頓擺動(dòng)畫效果的示例代碼的文章就介紹到這了,更多相關(guān)Flutter牛頓擺動(dòng)畫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android水波紋載入控件CircleWaterWaveView使用詳解
這篇文章主要為大家詳細(xì)介紹了Android水波紋載入控件CircleWaterWaveView使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01Android編程仿Iphone拖動(dòng)相片特效Gallery的簡單應(yīng)用示例
這篇文章主要介紹了Android編程仿Iphone拖動(dòng)相片特效Gallery的簡單應(yīng)用,結(jié)合實(shí)例形式分析了Android圖形拖動(dòng)特效的實(shí)現(xiàn)步驟與相關(guān)操作技巧,需要的朋友可以參考下2016-10-10利用Android從0到1實(shí)現(xiàn)一個(gè)流布局控件
新項(xiàng)目用到了一種全新布局,Android標(biāo)簽流式布局的功能,正好一直說給大家講自己定義控件的實(shí)現(xiàn),這篇文章主要給大家介紹了關(guān)于利用Android從0到1如何實(shí)現(xiàn)一個(gè)流布局控件的相關(guān)資料,需要的朋友可以參考下2021-08-08Android中CheckBox復(fù)選框控件使用方法詳解
這篇文章主要為大家詳細(xì)介紹了Android中CheckBox復(fù)選框控件的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Android編程之絕對布局AbsoluteLayout和相對布局RelativeLayout實(shí)例詳解
這篇文章主要介紹了Android編程之絕對布局AbsoluteLayout和相對布局RelativeLayout實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了Android絕對布局AbsoluteLayout和相對布局RelativeLayout的原理與使用技巧,需要的朋友可以參考下2015-12-12activitygroup 切換動(dòng)畫效果如何實(shí)現(xiàn)
本文將詳細(xì)介紹activitygroup 切換動(dòng)畫效果實(shí)現(xiàn)過程,需要聊解的朋友可以參考下2012-12-12Android自定義View實(shí)現(xiàn)多邊形統(tǒng)計(jì)圖示例代碼
這篇文章主要給大家介紹了關(guān)于Android自定義View如何實(shí)現(xiàn)多邊形統(tǒng)計(jì)圖的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01詳解 android 光線傳感器 light sensor的使用
這篇文章主要介紹了詳解 android 光線傳感器 light sensor的使用的相關(guān)資料,需要的朋友可以參考下2017-06-06Android編程實(shí)現(xiàn)在Activity中操作刷新另外一個(gè)Activity數(shù)據(jù)列表的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)在Activity中操作刷新另外一個(gè)Activity數(shù)據(jù)列表的方法,結(jié)合具體實(shí)例形式分析了2種常用的Activity交互實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-06-06Android Cocos Creator游戲開發(fā)平臺打包優(yōu)化實(shí)現(xiàn)方案
Cocos Creator是一款輕量、高效、免費(fèi)開源的跨平臺游戲引擎,同時(shí)也是實(shí)時(shí)3D內(nèi)容創(chuàng)作平臺,不僅支持2D、3D的游戲開發(fā),同時(shí)在HMI、IoT、XR、虛擬人偶等領(lǐng)域,均可提供一套完善的行業(yè)解決方案2022-11-11