What is Flutter?
Flutter 是 Google 用以幫助開發者在 iOS 和 Android 兩個平臺(現在是全平臺)開發高質量原生 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技能同時還能有一個滿意?的結果。
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
回撥來處理點選事件,這裡再進一步,再加上拖動效果。
垂直方向的手勢監聽可以通過onVerticalDragUpdate
來處理,根據返回的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、響應式程式設計方面的知識,歡迎關注我。
你可以在以下地方找到我:
簡書:www.jianshu.com/u/117f1cf0c…
Github: github.com/ditclear