您好,歡迎關注我的,本文章是關於 Flutter 的系列文,從簡單的 Flutter 介紹開始,一步步帶你瞭解進入 Flutter 的世界。你最好有一定的移動開發經驗,如果沒有也不要擔心,在我的專欄底部給我留言,我會盡我的能力給你解答。
上一篇專欄教大家如何在Flutter中拖拽View,並講解了會遇到的坑。這篇會深化View拖拽實例,利用Flutter Animation、插值器以及AnimatedBuilder教大家實現帶動畫的抽屜效果。先來看效果:
通過構思,我們可以設想到實現抽屜的方式就是用Stack控件將兩個Widget疊加顯示,用GestureDetector監聽手勢滑動,動態移動頂層的Widget,當監聽到手勢結束的時候根據手勢滑動的距離動態將頂部Widget利用動畫效果滑動到結束位置即可。
實現底部Widget
class DownDrawerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(child: Center(child: Text("底部Widget",),),);
}
}
這個Widget太簡單了,就不細說了。
實現頂部Widget
class UpDrawerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(child: Center(child: Text("頂部Widget",),),);
}
}
實現方式和底部是一樣的。
實現可以移動的容器
上面兩個Widget都是單純用來顯示的Widget,因此繼承了StatelessWidget。接下來我們需要根據手勢動態移動頂部的Widget,因此需要繼承StatefulWidget。
// 頂部Widget
class HomePageWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() => HomePageState();
}
class HomePageState extends State<HomePageWidget>
with SingleTickerProviderStateMixin {
@override
void initState() {...}
@override
void dispose() {...}
@override
Widget build(BuildContext context) {...}
void _onViewDragDown(DragDownDetails callback) {...}
void _onViewDrag(DragUpdateDetails callback) {...}
void _onViewDragUp(DragEndDetails callback) {...}
}
初始化狀態initState()
這個方法是在Widget初始化的時候系統的回調函數,我們需要在該函數中初始化動畫。
AnimationController controller;
@override
void initState() {
// 初始化動畫控制器,這裏限定動畫時常爲200毫秒
controller = new AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
// vsync對象會綁定動畫的定時器到一個可視的widget,所以當widget不顯示時,動畫定時器將會暫停,當widget再次顯示時,動畫定時器重新恢復執行,這樣就可以避免動畫相關UI不在當前屏幕時消耗資源。
// 當使用vsync: this的時候,State對象必須with SingleTickerProviderStateMixin或TickerProviderStateMixin;TickerProviderStateMixin適用於多AnimationController的情況。
// 設置動畫曲線,就是動畫插值器
// 通過這個鏈接可以瞭解更多差值器,https://docs.flutter.io/flutter/animation/Curves-class.html,我們這裏使用帶回彈效果的bounceOut。
CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
// 增加動畫監聽,當手勢結束的時候通過動態計算到達目標位置的距離實現動畫效果。curve.value爲當前動畫的值,取值範圍0~1。
curve.addListener(() {
double animValue = curve.value;
double offset = dragUpDownX - dragDownX;
double toPosition;
// 右滑
if (offset > 0) {
if (offset > maxDragX / 5) {
// 打開
toPosition = maxDragX;
isOpenState = true;
} else {
if (isOpenState) {
toPosition = maxDragX;
isOpenState = true;
} else {
toPosition = 0.0;
isOpenState = false;
}
}
} else {
if (offset < (-maxDragX / 2.0)) {
// 關
toPosition = 0.0;
isOpenState = false;
} else {
if (isOpenState) {
toPosition = maxDragX;
isOpenState = true;
} else {
toPosition = 0.0;
isOpenState = false;
}
}
}
dragOffset = (toPosition - dragUpDownX) * animValue + dragUpDownX;
// 刷新位置
setState(() {});
});
}
結束Widget dispose()
當Widget不可用將被回收的時候,系統會回調dispose()方法,我們在這裏回收動畫。
@override
void dispose() {
controller.dispose();
}
記錄按下的位置
double dragDownX = 0.0;
void _onViewDragDown(DragDownDetails callback) {
dragDownX = callback.globalPosition.dx;
}
拖動的時候刷新View的位置
/**
* 最大可拖動位置
*/
final double maxDragX = 230.0;
double dragOffset = 0.0;
void _onViewDrag(DragUpdateDetails callback) {
double tmpOffset = callback.globalPosition.dx - dragDownX;
if (tmpOffset < 0) {
tmpOffset += maxDragX;
}
// 邊緣檢測
if (tmpOffset < 0) {
tmpOffset = 0.0;
} else if (tmpOffset >= maxDragX) {
tmpOffset = maxDragX;
}
// 刷新
if (dragOffset != tmpOffset) {
dragOffset = tmpOffset;
setState(() {});
}
}
離手的時候記錄位置並執行動畫
/**
* 脫手時候的位置
*/
double dragUpDownX = 0.0;
void _onViewDragUp(DragEndDetails callback) {
dragUpDownX = dragOffset;
// 執行動畫,每次都從第0幀開始執行
controller.forward(from: 0.0);
}
支持移動的Widget
@override
Widget build(BuildContext context) {
return Transform.translate(
offset: Offset(dragOffset, 0.0),
child: Container(
child: GestureDetector(
onHorizontalDragDown: _onViewDragDown,
onVerticalDragDown: _onViewDragDown,
onHorizontalDragUpdate: _onViewDrag,
onVerticalDragUpdate: _onViewDrag,
onHorizontalDragEnd: _onViewDragUp,
onVerticalDragEnd: _onViewDragUp,
child: Container(
child: new UpDrawerWidget(),
),),),);}
Flutter動畫
總結一下,想在Flutter中實現動畫,需要先創建一個AnimationController控制器;如果有特殊的插值要求,再創建一個插值器,調用controller.forward()方法執行動畫,通過addListener()的回調改變對應數值之後調用setState(() {})方法刷新位置即可。
Flutter API還提供AnimatedBuilder用來簡化實現動畫的複雜性,讓我們不用手動調用addListener()方法。
本篇專欄介紹瞭如何用Flutter實現抽屜效果,並順帶介紹了Flutter中的動畫。下一篇專欄我會教大家如何用Flutter實現左右滑動的日曆。