這篇會深化View拖拽實例,利用Flutter Animation、插值器以及AnimatedBuilder教你們實現帶動畫的抽屜效果。先來看效果:html
經過構思,咱們能夠設想到實現抽屜的方式就是用Stack控件將兩個Widget疊加顯示,用GestureDetector監聽手勢滑動,動態移動頂層的Widget,當監聽到手勢結束的時候根據手勢滑動的距離動態將頂部Widget利用動畫效果滑動到結束位置便可。java
class DownDrawerWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container(child: Center(child: Text("底部Widget",),),); } }
這個Widget太簡單了,就不細說了。web
class UpDrawerWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container(child: Center(child: Text("頂部Widget",),),); } }
實現方式和底部是同樣的。less
上面兩個Widget都是單純用來顯示的Widget,所以繼承了StatelessWidget。接下來咱們須要根據手勢動態移動頂部的Widget,所以須要繼承StatefulWidget。ide
// 頂部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) {...} }
這個方法是在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()方法,咱們在這裏回收動畫。動畫
@override void dispose() { controller.dispose(); }
double dragDownX = 0.0; void _onViewDragDown(DragDownDetails callback) { dragDownX = callback.globalPosition.dx; }
/** * 最大可拖動位置 */ 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); }
@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中實現動畫,須要先建立一個AnimationController控制器;若是有特殊的插值要求,再建立一個插值器,調用controller.forward()方法執行動畫,經過addListener()的回調改變對應數值以後調用setState(() {})方法刷新位置便可。
Flutter API還提供AnimatedBuilder用來簡化實現動畫的複雜性,讓咱們不用手動調用addListener()方法。ui