flutter中自帶了drawer元件,可以實現通用的選單功能,那麼有沒有一種可能,我們可以通過自定義動畫來實現一個別樣的選單呢?
答案是肯定的,一起來看看吧。
因為這裡的主要目的是實現選單的動畫,所以這裡的選單比較簡單,我們的menu是一個StatefulWidget,裡面就是一個Column元件,column中有四行詩:
static const _menuTitles = [
'遲日江山麗',
'春風花草香',
'泥融飛燕子',
'沙暖睡鴛鴦',
];
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child:_buildContent()
);
}
Widget _buildContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
..._buildListItems()
],
);
}
List<Widget> _buildListItems() {
final listItems = <Widget>[];
for (var i = 0; i < _menuTitles.length; ++i) {
listItems.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 36.0, vertical: 16),
child: Text(
_menuTitles[i],
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
),
),
)
);
}
return listItems;
}
怎麼讓menu動起來呢?我們需要給最外層的AnimateMenuApp新增一個AnimationController,所以需要在_AnimateMenuAppState新增SingleTickerProviderStateMixin的mixin,如下所示:
class _AnimateMenuAppState extends State<AnimateMenuApp>
with SingleTickerProviderStateMixin {
late AnimationController _drawerSlideController;
然後在initState中對_drawerSlideController進行初始化:
void initState() {
super.initState();
_drawerSlideController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
}
在讓menu動起來之前,我們需要設計一下動畫的樣式。假如我們的動畫是讓menu從右向左飛出。那麼我們可以使用FractionalTranslation來進行offset進行位置變換。
並且當選單沒有開啟的時候,我們需要顯示一個空的元件,這裡用SizedBox來替代。
當選單開啟的時候,就執行這個FractionalTranslation的動畫,所以我們的build方法需要這樣寫:
Widget _buildDrawer() {
return AnimatedBuilder(
animation: _drawerSlideController,
builder: (context, child) {
return FractionalTranslation(
translation: Offset(1.0 - _drawerSlideController.value, 0.0),
child: _isDrawerClosed() ? const SizedBox() : const Menu(),
);
},
);
}
FractionalTranslation中的Offset是根據_drawerSlideController的value來進行變化的。
那麼_drawerSlideController的value怎麼變化呢?
我們定義一個_toggleDrawer方法,在點選選單按鈕的時候來觸發這個方法,從而實現_drawerSlideController的value變化:
void _toggleDrawer() {
if (_isDrawerOpen() || _isDrawerOpening()) {
_drawerSlideController.reverse();
} else {
_drawerSlideController.forward();
}
}
同時,我們定義下面幾個判斷選單狀態的方法:
bool _isDrawerOpen() {
return _drawerSlideController.value == 1.0;
}
bool _isDrawerOpening() {
return _drawerSlideController.status == AnimationStatus.forward;
}
bool _isDrawerClosed() {
return _drawerSlideController.value == 0.0;
}
因為選單圖示需要根據選單狀態來發生改變,選單的狀態又是依賴於_drawerSlideController,所以,我們把IconButton放到一個AnimatedBuilder裡面,從而實現動態變化的效果:
PreferredSizeWidget _buildAppBar() {
return AppBar(
title: const Text(
'動畫選單',
style: TextStyle(
color: Colors.black,
),
),
backgroundColor: Colors.transparent,
elevation: 0.0,
automaticallyImplyLeading: false,
actions: [
AnimatedBuilder(
animation: _drawerSlideController,
builder: (context, child) {
return IconButton(
onPressed: _toggleDrawer,
icon: _isDrawerOpen() || _isDrawerOpening()
? const Icon(
Icons.clear,
color: Colors.black,
)
: const Icon(
Icons.menu,
color: Colors.black,
),
);
},
),
],
);
}
最後實現的效果如下:
上面的例子中整個選單是作為一個整體來動畫的,有沒有可能選單裡面的每一個item也有自己的動畫呢?
答案當然是肯定的。
我們只需要在上面的基礎上將menu元件新增動畫支援即可:
class _MenuState extends State<Menu> with SingleTickerProviderStateMixin
動畫中的位移我們選擇使用Transform.translate,同時還新增了淡入淡出的效果,也就是把上面例子中的Padding用AnimatedBuilder包裹起來,如下所示:
List<Widget> _buildListItems() {
final listItems = <Widget>[];
for (var i = 0; i < _menuTitles.length; ++i) {
listItems.add(
AnimatedBuilder(
animation: _itemController,
builder: (context, child) {
final animationPercent = Curves.easeOut.transform(
_itemSlideIntervals[i].transform(_itemController.value),
);
final opacity = animationPercent;
final slideDistance = (1.0 - animationPercent) * 150;
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(slideDistance, 0),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 36.0, vertical: 16),
child: Text(
_menuTitles[i],
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
return listItems;
}
AnimatedBuilder中的builder返回的是一個Opacity物件,裡面包含了opacity和child兩個屬性。其中最終要的一個變化值是animationPercent,這個值是根據_itemController的value和初始設定的各個item的變化時間來決定的。
每個item的值是不一樣的:
void _createAnimationIntervals() {
for (var i = 0; i < _menuTitles.length; ++i) {
final startTime = _initialDelayTime + (_staggerTime * i);
final endTime = startTime + _itemSlideTime;
_itemSlideIntervals.add(
Interval(
startTime.inMilliseconds / _animationDuration.inMilliseconds,
endTime.inMilliseconds / _animationDuration.inMilliseconds,
),
);
}
}
最後執行結果如下:
在flutter中一切皆可動畫,我們只需要掌握動畫創作的訣竅即可。
本文的例子:https://github.com/ddean2009/learn-flutter.git