最右JS2Flutter框架——動畫、小遊戲的實現(四)

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"1、概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"動畫和小遊戲看起來是兩個不太相關的話題,但其實它們都依賴於Vsync機制的建立,對動畫依賴於Vsync機制不太理解的同學,可以查看Gityuan的博客——"},{"type":"link","attrs":{"href":"http://gityuan.com/2019/07/13/flutter_animator/","title":""},"content":[{"type":"text","text":"深入理解Flutter動畫原理"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[1],最右目前所採用的小遊戲引擎是"},{"type":"link","attrs":{"href":"https://github.com/flame-engine/flame","title":""},"content":[{"type":"text","text":"Flame"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[2],其GameLoop也是藉助於Ticker(依賴Vsync)實現Game的不斷刷新。可見要實現動畫和小遊戲,我們必須給Client側提供Vsync機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"2、Vsync機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"我們先看看Flutter是如何建立Vsync機制的,在"},{"type":"link","attrs":{"href":"http://gityuan.com/2019/07/13/flutter_animator/","title":""},"content":[{"type":"text","text":"深入理解Flutter動畫原理"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[1]文章中,雖然着重點是在動畫流程上,但提到了註冊Vsync,比較細心的同學可能會發現文末那張圖的Choreographer,Choreographer的作用就是接收底層的Vsync信號,爲上層App的渲染提供穩定的時機,這點信息Android同學應該很快能捕捉到。我們再去求證一下,Android端在VsyncWaiter裏利用Choreographer給Flutter提供了Vsync時機。"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":" private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate = new FlutterJNI.AsyncWaitForVsyncDelegate() {\n @Override\n public void asyncWaitForVsync(long cookie) {\n Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {\n @Override\n public void doFrame(long frameTimeNanos) {\n float fps = windowManager.getDefaultDisplay().getRefreshRate();\n long refreshPeriodNanos = (long) (1000000000.0 / fps);\n FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);\n }\n });\n }\n };"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"而我們是在iOS端,要給Client側提供Vsync時機,我們去看看iOS端Flutter是如何實現的,iOS端的實現在flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm裏面。"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"- (instancetype)initWithTaskRunner:(fml::RefPtr<:taskrunner>)task_runner\n callback:(flutter::VsyncWaiter::Callback)callback {\n self = [super init];\n\n if (self) {\n callback_ = std::move(callback);\n display_link_ = fml::scoped_nsobject {\n [[CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)] retain]\n };\n display_link_.get().paused = YES;\n\n task_runner->PostTask([client = [self retain]]() {\n [client->display_link_.get() addToRunLoop:[NSRunLoop currentRunLoop]\n forMode:NSRunLoopCommonModes];\n [client release];\n });\n }\n\n return self;\n}\n\n...\n\n- (void)onDisplayLink:(CADisplayLink*)link {\n fml::TimePoint frame_start_time = fml::TimePoint::Now();\n fml::TimePoint frame_target_time = frame_start_time + fml::TimeDelta::FromSecondsF(link.duration);\n\n display_link_.get().paused = YES;\n\n callback_(frame_start_time, frame_target_time);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"到這裏大家應該都明白了,我們也可以用跟系統一樣的方式,利用"},{"type":"text","text":"CADisplayLink"},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"在Native給Client側建立起Vsync機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"3、Animation"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"JS2Flutter框架是由Client側去驅動Host側的渲染的,想要實現UI上的變化基本上都是Client側的虛擬樹發生變化,從而驅動Host側真實Widget樹的變化。很多同學可能會想到,可以在動畫插值過程中,通過不斷的重建虛擬樹去實現動畫,但其實這種做法是效率很低的,也沒必要,動畫只是影響Widget樹邊界的形變(矩陣變換),並不會引起Widget樹結構的變化,所以我們可以只讓Host側真實的Widget做這個動畫,Client側保證動畫的值和狀態實時更新,保證邏輯上的正確性就可以了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"要讓真實的Widget樹執行動畫,就意味着必須在Host側構建真實的Animation、AnimationController,在Client側只是純粹的Api代理,我們只需要把Client側創建Animation、AnimationController和Host側的真身對應起來即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"AnimationController的構造還依賴於TickerProvider,當Client側的AnimationController創建時,我們也需要在Host側創建真身,那真身依賴的TickerProvider該從何而來呢?還記得我們在"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/5c2dbdac0a27bb55863d0be25","title":""},"content":[{"type":"text","text":"最右JS2Flutter框架——渲染機制"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[3]中AppLifecycleState的實現嗎?藉助AppContainer,由於它的生命週期等於整個Flutter App的生命週期,可以用它來提供可靠的TickerProvider。另一個問題就是保證Client側Animation的值和狀態的準確性,藉助我們在上一篇文章"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/f23e562e3aa7f3c198eb40a83","title":""},"content":[{"type":"text","text":"最右JS2Flutter框架通信機制"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[4]中講述的雙向同步通信機制,可以通過監聽真實Animation的變化,從而同步修改Client側Animation。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"很多業務場景需要監聽Animation的更新去做UI上的變化,在這種使用場景下,難免會帶來虛擬樹的重建,我們儘可能做更小粒度的Widget樹更新。舉個例子,我們要實現一個翻卡動畫,當動畫執行到一半的時候,我們需要將背面顯示出來,這種情況我們只做卡片內容的更新。"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":" Widget build(BuildContext context) {\n final front = widget.childFront;\n final back = widget.childBack;\n Matrix4 transform = Matrix4.identity()..rotateY(_animation.value);\n return AnimatedBuilder(\n animation: _animation,\n builder: (BuildContext context, Widget child) {\n return Transform(\n transform: transform,\n alignment: Alignment.center,\n child: IndexedStack(\n alignment: Alignment.center,\n children: [\n front,\n back,\n ],\n index: _animationCtr.value < 0.5 ? 0 : 1,\n ),\n );\n },\n );\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"4、小遊戲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"最右目前所採用的小遊戲引擎是"},{"type":"link","attrs":{"href":"https://github.com/flame-engine/flame","title":""},"content":[{"type":"text","text":"Flame"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[2],要實現小遊戲的能力,我們必須先對Flame的實現有一定了解,尤其是Flame是如何去繪製的,這裏直接拋出結論,有興趣的同學可以去查看源碼,其實核心就在這裏:"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"class GameRenderBox extends RenderBox with WidgetsBindingObserver {\n BuildContext context;\n Game game;\n GameLoop gameLoop;\n\n GameRenderBox(this.context, this.game) {\n gameLoop = GameLoop(gameLoopCallback);\n }\n\n ...\n\n void gameLoopCallback(double dt) {\n if (!attached) {\n return;\n }\n game.recordDt(dt);\n game.update(dt);\n markNeedsPaint();\n }\n\n @override\n void paint(PaintingContext context, Offset offset) {\n context.canvas.save();\n context.canvas.translate(\n game.builder.offset.dx + offset.dx, game.builder.offset.dy + offset.dy);\n game.render(context.canvas);\n context.canvas.restore();\n }\n\n ...\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"每次Vsync的時候,會回調gameLoopCallback,每次都會標記刷新,把Game的Component畫到Canvas上,Component會確定自己的位置以及所繪製的內容,小遊戲的渲染都是通過Canvas去繪製,所以我們先要支持Canvas能力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"我們先看看Flutter是如何實現Canvas的,我們以rotate爲例:"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"void rotate(double radians) native 'Canvas_rotate';"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"Framework層提供的Canvas,最終實際上調到了Engine層flutter/lib/ui/painting/canvas.cc的同名函數,進而調用SkCanvas的同名函數。我們也採用相同的策略,Client側聲明鏡像的Canvas,提供與Flutter Canvas對等的能力,Client側鏡像Canvas函數的調用,直接通過通信渠道轉化爲Flutter Canvas函數的調用。最右爲了實現Canvas的高效繪製,對於Canvas指令的數據化採用StandardMessageCodec去實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"所以我們只需要按照Flame的思路去實現就好了,當Native通知Client側Vsync的時候,收集畫在Canvas上的指令,然後把這些指令通過StandardMessageCodec數據化,傳遞到Host側,再把指令解析出來,還原這些指令操作,讓Host側預佔坑的Game繪製到Canvas上即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"5、結束語"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"本文主要闡述了JS2Flutter框架Vsync機制的建立,以及Animation和小遊戲的實現。綜合前面的幾篇文章,相信大家對JS2Flutter框架有了更多的瞭解,希望能對大家有所啓發和幫助,最右將在Flutter動態化道路上持續探索,歡迎關注。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"6、參考文獻"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[1]:深入理解Flutter動畫原理 "},{"type":"link","attrs":{"href":"http://gityuan.com/2019/07/13/flutter_animator/","title":""},"content":[{"type":"text","text":"http://gityuan.com/2019/07/13/flutter_animator/"}],"marks":[{"type":"size","attrs":{"size":12}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[2]:Flame "},{"type":"link","attrs":{"href":"https://github.com/flame-engine/flame","title":""},"content":[{"type":"text","text":"https://github.com/flame-engine/flame"}],"marks":[{"type":"size","attrs":{"size":12}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[3]:最右JS2Flutter框架——渲染機制 "},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/5c2dbdac0a27bb55863d0be25","title":""},"content":[{"type":"text","text":"https://xie.infoq.cn/article/5c2dbdac0a27bb55863d0be25"}],"marks":[{"type":"size","attrs":{"size":12}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[4]:最右JS2Flutter框架——通信機制 "},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/f23e562e3aa7f3c198eb40a83","title":null},"content":[{"type":"text","text":"https://xie.infoq.cn/article/f23e562e3aa7f3c198eb40a83"}],"marks":[{"type":"size","attrs":{"size":12}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章