使用Flutter來完成Uplabs上炫酷的互動
What is Flutter?
Flutter 是 Google 用以幫助開發者在
(現在是全平臺)開發高質量原生 UI 的移動 SDK。Flutter 相容現有的程式碼,免費並且開源,在全球開發者中廣泛被使用.
What is Uplabs?
Uplabs是設計師和開發人員尋找,分享和購買靈感和資源以構建應用和網站的地方。
在這裡,每個人都可以:
1.瀏覽並在我們的Material Design(Android),iOS和macOS以及Site的日常展示中找到靈感。
2.搜尋特定的UI元素和解決方案;
3.分享她/他的作品(設計,圖書館,片段,應用程式,網站)作為靈感或免費贈品;
4.出售她/他的作品(主題,模板,圖示等)。

Uplabs上有很多免費的設計圖,開發人員可以選取其中一個,比如說Google maps。

可以看到右邊有 xd 的圖示,表示該專案下載下來之後需要用Adobe XD 開啟,當然也支援sketch、PS、Figma等等。

開啟之後,點選右上角的預覽按鈕能夠預覽原型互動,對於實現互動細節非常重要。

你還可以將設計稿通過外掛(比如藍湖XD)匯出到藍湖平臺,相當於一個免費的UI大師就位了。

總的來說,對於Flutter開發者而言,這裡就是一座寶庫。
許多用原生技術都難以實現或者較難實現的互動,運用Flutter,在鍛鍊你的Flutter技能同時還能有一個滿意:blush:的結果。
How to implement ?
我們可以來實現一個簡單的過渡效果

問題:現在通過UI圖可以得知正方形的初始大小為100,起始位置為居中、距離底部100px,經過過渡後的位置為居中、距離底部500px,同時大小改為300,設定圓角為30.
知道了起點和終點,我們可以結合Stack和Positioned來完成位置的變化。
Stack( children: <Widget>[ Positioned( bottom: 100, left: (screenWidth - 100) / 2, //center width: 100, height: 100, child: DecoratedBox( decoration: BoxDecoration( color: Colors.red, border: Border.all(), borderRadius: BorderRadius.all(Radius.circular(0)), ), ), ), ], )
接著我們來完成動畫,你可以選擇組合多個動畫,但這樣會稍顯麻煩,其實我們只需要確定一個動畫,其它的動畫只是附帶引起的變化而已。
這裡選用 bottom
的偏移進行動畫,開始的時候距離底部為100,結束之後距離底部為500,時間我們挑選為500毫秒。
AnimationController animationController; Animation animation; //offset bottom double offset = 0; @override void initState() { super.initState(); animationController = AnimationController(duration: Duration(milliseconds: 500), vsync: this); animation = Tween<double>(begin: 0.0, end: 500.0-100.0).animate(animationController) ..addListener(() { // notify ui update setState(() { offset = animation.value; }); }); }
當動畫進行時, offset
就可以更新為動畫這時候的值,然後通過 setState
通知UI更新。
這時,就需要更改bottom的表示式為:
bottom: 100->bottom:100+offset
但是為了引起正方形其它引數的變化,因此,我們最好是得到一個offset佔總偏移量的比重。
get currentPercent => offset / (500.0-100.0);
接著我們的表示式也可以用另一種形式來寫:
bottom: 100->bottom:100+offset-> bottom:100+(500.0-100.0)*currentPercent
用這樣的邏輯,我們便可以完成上述的過渡效果。
Stack( children: <Widget>[ Positioned( // start 100, center end 500 center bottom: 100 + (500 - 100) * currentPercent, left: (screenWidth - (100 + (300 - 100) * currentPercent)) / 2, width: 100 + (300 - 100) * currentPercent, height: 100 + (300 - 100) * currentPercent, child: GestureDetector( onTap: () { if (animationController.status == AnimationStatus.completed) { animationController.reverse(); } else { animationController.forward(); } }, child: DecoratedBox( decoration: BoxDecoration( color: Colors.red, border: Border.all(), borderRadius: BorderRadius.all(Radius.circular(30 * currentPercent))))), ) ], )
處理手勢
在上面的程式碼中我們已經套上了一層 GestureDetector
,然後通過 onTap
回撥來處理點選事件,這裡再進一步,再加上拖動效果。

垂直方向的手勢監聽可以通過 onVerticalDragEnd
來處理,根據返回的 DragUpdateDetails
引數,可以獲取的滑動距離,我們可以根據它來改變offset。
onVerticalDragUpdate: (details) { // scrollUp means -= offset -= details.delta.dy; if (offset > 400) { offset = 400; } else if (offset < 0) { offset = 0; } setState(() {}); },
當手指離開螢幕的時候,我們再根據offset的大小和狀態通過動畫移動到合適的位置。
需要注意的是動畫開始的值也就是begin是變化的,因此我們的動畫也需要動態建立。
onVerticalDragEnd: (_) { if (isEnd) { if (currentPercent >= 0.7) { animate(true); } else { animate(false); } } else { if (currentPercent >= 0.3) { animate(true); } else { animate(false); } } },
isEnd
代表處於結束位置,再來看看動畫。
/// 滑動到開始或結束位置,Swipe to the start or end position /// /// [end] true is the end position, otherwise the start position /// [end] 為true是結束位置 反之是開始位置 void animate(bool end) { animationController = AnimationController( duration: Duration(milliseconds: (1 + 500 * (isEnd ? currentPercent : (1.0 - currentPercent))).toInt()), vsync: this); animation = Tween<double>(begin: offset, end: end ? 400.0 : 0.0).animate(animationController) ..addListener(() { setState(() { offset = animation.value; }); }) ..addStatusListener((status) { if (status == AnimationStatus.completed) { isEnd = !isEnd; } }); animationController.forward(); }
begin
的值都是 offset
,只是 end
的值需要通過是滑動到開始或結束位置而改變, 需要注意的就是動畫時間也需要根據偏移量offset有所變化。
其它更為複雜的互動也不過是同一個套路,你可以檢視 flutter_challenge_googlemaps 來了解它,效果圖如下:

Join in Flutter-UI-Challenges
為了讓更多的開發者嘗試Flutter技術,在體會到Flutter魅力的同時完成精美的互動,我在GitHub上建立了 Flutter-UI-Challenges 這個組織,開發者可以通過實現Uplabs中一個UI挑戰來加入我們。
如果你完成了其中一個挑戰,恭喜你,如果你想提交併加入我們,那麼可以在 JoinUs 中提Issue,格式如下:

Issue名稱的格式為 flutter_chanllenge_xx ,比如 flutter_challenge_googlemaps
.
內容請附上 Uplabs 上UI挑戰的網址 和 GitHub相應實現的網址 。
注意: 請給Issue打上joinus標籤。
我們會對其進行評審以決定是否可以通過,評審內容包括:
- 效果是否相符?
完成度至少在80%以上,
- 質量
我們不僅要求能實現精美的互動效果,同時也追求更高的程式碼的質量,完善且符合dart規範的註釋和精簡有力的程式碼是我們的追求。
- 符合規範
專案名請以 flutter_chanllenge_ 開頭
Readme的格式請參考 flutter_challenge_googlemaps
要求有符合Dart文件規範的註釋和合理的程式碼拆分。
最後
期待您的加入。
==================== 分割線 ======================
如果你想了解更多關於MVVM、Flutter、響應式程式設計方面的知識,歡迎關注我。
你可以在以下地方找到我:
簡書: https://www.jianshu.com/u/117f1cf0c556
掘金: https://juejin.im/user/582d601d2e958a0069bbe687
Github: https://github.com/ditclear
