效果图
前言
Hello,各位小伙伴,临近过年,导师不催写进度汇报了,就动手玩了几下Flutter,刚好我自己开发的软件需要新增一个创建文件夹的功能,再加上原来有的创建文件的功能,一共有两个功能了,因此原来的FloatingActionButton已经不能满足需求了。在掘金搜寻无果,决定自己撸了一个FloatingActionMenu。现在与大家一起交流。
正文
1. 创建dart文件
首先我们创建一个StatefulWidget。目前就暂定叫FloatingActionMenu吧。在创建好的空dart文件输入st,选中stful,Android Studio就会为我们创建好一个模板了。
class FloatingActionMenu extends StatefulWidget {
const FloatingActionMenu({Key? key}) : super(key: key);
@override
State<FloatingActionMenu> createState() => _FloatingActionMenuState();
}
class _FloatingActionMenuState extends State<FloatingActionMenu> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
2. 创建菜单
有了模板之后,我们就需要创建一个Container,把原来的菜单按钮显示出来先。
修改之后的 _FloatingActionMenuState 代码如下:
class _FloatingActionMenuState extends State<FloatingActionMenu> {
@override
Widget build(BuildContext context) {
return mainButton();
}
Widget mainButton() {
return Positioned(
bottom: 0,
child: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.all(Radius.circular(56)),
),
child: Center(
child: Icon(
Icons.add,
color: Colors.white,
),
),
),
);
}
}
然后在main.dart下面,让Scaffold中的FloatingActionMenu传给 floatingActionButton。
代码如下:
import "package:flutter/material.dart";
import 'package:learn/animation_widget/floating_action_menu.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('小案例'),
),
floatingActionButton: FloatingActionMenu(),
),
//去掉右上角的debug贴纸
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.blue),
);
}
}
效果图如下:
3. 创建子按钮
子按钮我们要定义好按钮的图标、背景、以及点击以后的回调事件。这里子按钮我们设置为了42其实是不对的,应该是40。起码Material Design官方是这样定义的。
class SubFloatActionButton extends StatelessWidget {
final VoidCallback callback;
final Color backgroundColor;
final content;
const SubFloatActionButton(
{Key? key,
required this.callback,
required this.backgroundColor,
required this.content})
: super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: callback,
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(56)),
boxShadow: [
BoxShadow(
blurRadius: 10.0,
offset: Offset(0, 10),
color: backgroundColor.withAlpha(100))
]),
child: content,
));
}
}
4.创建主按钮动画
按钮展开以前是这样的,
展开以后是这样的,
小伙伴应该已经猜到要用什么了,对,Flutter动画。
使用Flutter中的Animation就需要混入一个TickerProviderStateMixin,因此State继承代码修改如下:
class _FloatingActionMenuState extends State<FloatingActionMenu>
with TickerProviderStateMixin{}
然后我们声明两个私有变量:
late AnimationController _animationController; // 控制动画的类
late Animation<double> _animation;
并在initState中完成初始化:
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
_animation = Tween(begin: 0.0, end: 1.0).animate(_animationController);
随后,在原来的mainButton基础上,将Icon改成AnimatedIcon
即
AnimatedIcon(
color: Colors.white,
icon: AnimatedIcons.menu_close,
progress: _animation,
)
最终把菜单按钮改为如下:
Widget mainButton() {
return Positioned( // 这里的Position后续会增加,可先删除
bottom: 0,
child: Hero(
tag: widget.heroTag, // 这里后续会增加,可先删除
child: GestureDetector(
onTap: () {
if (_animationController.isCompleted) {
_closeMenu();
setState(() {
_left = 0.0;
});
} else {
_openMenu();
setState(() {
_left = 1.0;
});
}
print("点击 $_left");
},
child: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(56)),
boxShadow: [
BoxShadow(
blurRadius: 10.0,
offset: Offset(0, 10),
color: widget.backgroundColor.withAlpha(100))
]),
child: Center(
child: AnimatedIcon(
color: Colors.white,
icon: AnimatedIcons.menu_close,
progress: _animation,
),
),
)),
),
);
}
随后增加点击事件,用于动画控制器的顺序播放和倒序播放。代码如下:
_openMenu() {
_animationController.forward();
}
_closeMenu() {
_animationController.reverse();
}
5.创建按钮布局
在创建布局之前,先将封装好。
class FloatingActionMenu extends StatefulWidget {
final Color backgroundColor; // 菜单的背景颜色
final String heroTag; //共享动画
final List<SubFloatActionButton> children; // 子按钮,即那个小按钮
FloatingActionMenu(
{Key? key,
required this.backgroundColor,
required this.children,
this.heroTag = 'JayFloatActionButton'})
: super(key: key);
@override
State<FloatingActionMenu> createState() => _FloatingActionMenuState();
}
我们这里主要用一个Stack来进行布局。
将build中的代码修改如下:
@override
Widget build(BuildContext context) {
// 得在这里装载数据,不然动画不显示
_buttons.clear();
for (int i = 0; i < widget.children.length; i++) {
_buttons.add(subButton(widget.children[i], i));
}
_buttons.add(mainButton());
return Container(
height: 200,
width: 56,
// color: Colors.grey,
child: Stack(children: _buttons),
);
}
6.添加动画
首先添加一个按钮链表。
List<Widget> _buttons = [];
添加一个subButton,用来包装原来的按钮,添加动画组件。需要变化的时候,直接setState()就可以了。代码如下:
double _left = 0.0;
Widget subButton(SubFloatActionButton subFloatActionButton, int i) {
return AnimatedPositioned(
bottom: (56 + (10.0 * (i + 1)) + 42 * i) * _left,
left: (56 - 42) / 2,
duration: Duration(milliseconds: 300),
child: FadeTransition(
opacity: _animation,
child: subFloatActionButton,
),
);
}
全部代码
所有代码如下,可以直接使用了。
FloatingActionMenu所有代码
import 'package:flutter/material.dart';
class FloatingActionMenu extends StatefulWidget {
final Color backgroundColor;
final String heroTag;
final List<SubFloatActionButton> children;
FloatingActionMenu(
{Key? key,
required this.backgroundColor,
required this.children,
this.heroTag = 'JayFloatActionButton'})
: super(key: key);
@override
State<FloatingActionMenu> createState() => _FloatingActionMenuState();
}
class _FloatingActionMenuState extends State<FloatingActionMenu>
with TickerProviderStateMixin {
late AnimationController _animationController; // 控制动画的类
late Animation<double> _animation;
List<Widget> _buttons = [];
@override
void initState() {
// if(widget.children!=null) {
// }
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
_animation = Tween(begin: 0.0, end: 1.0).animate(_animationController);
// for (int i = 0; i < widget.children.length; i++) {
// _buttons.add(subButton(widget.children[i], i));
// }
// _buttons.add(mainButton());
super.initState();
}
@override
Widget build(BuildContext context) {
_buttons.clear();
for (int i = 0; i < widget.children.length; i++) {
_buttons.add(subButton(widget.children[i], i));
}
_buttons.add(mainButton());
return Container(
height: 200,
width: 56,
// color: Colors.grey,
child: Stack(children: _buttons),
);
}
double _left = 0.0;
Widget subButton(SubFloatActionButton subFloatActionButton, int i) {
return AnimatedPositioned(
bottom: (56 + (10.0 * (i + 1)) + 42 * i) * _left,
left: (56 - 42) / 2,
duration: Duration(milliseconds: 300),
child: FadeTransition(
opacity: _animation,
child: subFloatActionButton,
),
);
}
Widget mainButton() {
return Positioned(
bottom: 0,
child: Hero(
tag: widget.heroTag,
child: GestureDetector(
onTap: () {
if (_animationController.isCompleted) {
_closeMenu();
setState(() {
_left = 0.0;
});
} else {
_openMenu();
setState(() {
_left = 1.0;
});
}
print("点击 $_left");
},
child: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(56)),
boxShadow: [
BoxShadow(
blurRadius: 10.0,
offset: Offset(0, 10),
color: widget.backgroundColor.withAlpha(100))
]),
child: Center(
child: AnimatedIcon(
color: Colors.white,
icon: AnimatedIcons.menu_close,
progress: _animation,
),
),
)),
),
);
}
_openMenu() {
_animationController.forward();
}
_closeMenu() {
_animationController.reverse();
}
}
class SubFloatActionButton extends StatelessWidget {
final VoidCallback callback;
final Color backgroundColor;
final content;
const SubFloatActionButton(
{Key? key,
required this.callback,
required this.backgroundColor,
required this.content})
: super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: callback,
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(56)),
boxShadow: [
BoxShadow(
blurRadius: 10.0,
offset: Offset(0, 10),
color: backgroundColor.withAlpha(100))
]),
child: content,
));
}
}
使用
import "package:flutter/material.dart";
import 'package:learn/animation_widget/float_action_menu.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('小案例'),
),
// body: CustomCheckboxTest(),
floatingActionButton: FloatingActionMenu(
backgroundColor: Colors.orange,
children: [
SubFloatActionButton(
callback: () {},
backgroundColor: Colors.orange,
content: Icon(
Icons.file_open,
color: Colors.white,
)),
SubFloatActionButton(
callback: () {
print("hello");
},
backgroundColor: Colors.orange,
content: Icon(
Icons.folder,
color: Colors.white,
)),
],
),
),
//去掉右上角的debug贴纸
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.blue),
);
}
}