Flutter-FloatCircleMenu
1、Requirement
To implements this view, in Android platform,we can self-design a custom circle menu by canvas,furthermore,the material design can do a favor.The view contains animations,including scale,rotate,translate. On center icon opening,it turn to close icon with scale animation,and child items spread out to pointed angle with rotation and translate animation and re-clicked center icon,the child animation are executed in reversed.
Finally,we need to package our widget,and expose partial interface to other widgets.
the final destination is taken on below:
Obviously,we need implements it in flutter.Fortunately,i search the similar library in github and other websites and this guy's blog save my days.Whereas his codes can not apply directly,so we need to rebuild.
2、Coding
While reading the fuck source code,we will start coding which base on the origin code.
(1)Animation
Basically,the fundation of this function is animation,we design the animation first.
- AnimationController
controller = new AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
- scale animation
scale = Tween<double>(
begin: 1,
end: 0.0,
).animate(
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn),
)..addListener(() {
setState(() {});
});
-rotate animation
rotation = Tween<double>(
begin: 0.0,
end: 360.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.3,
0.9,
curve: Curves.decelerate,
),
),
)..addListener(() {
setState(() {});
});
- translate animation
translation = Tween<double>(
begin: 0.0,
end: 100.0,
).animate(
CurvedAnimation(parent: controller, curve: Curves.linear),
)..addListener(() {
setState(() {});
});
Now,we copy the origin codes,however we add listener to each animation,which contains setState method,it can trigger widget to update itself with variable values which produced by runnning-animation.
(2)Child Item Position
The child item can be one or more,and it can be placed around center icon with 8 positions,including left,right,top,bottom,left-top,left-bottom,right-top,right-bottom.Each item must execute rotate and transform animation, additionally,it can be response to clicked.
the codes below:
var childWidget = Transform(
transform: Matrix4.identity()
..translate(
(translation.value) * cos(rad), (translation.value) * sin(rad)),
child: Transform.rotate(
// Add rotation
angle: radians(rotation.value),
child: FloatingActionButton(
child: _itemDetail.childIcon,
onPressed: () {
widget.itemClicked(_itemDetail.itemOption);
},
elevation: 0)));
In above code,the angle should be converted to radians,the principle is:
Rad = PI / 180 * Degree
Factly,we take a few radians into consideration:x * PI/4,x are integreted and from zero to seven.
the child item translation point dx is cos,and dy is sin,and then Multiply by base value.The rotate animation was placed at child parameters for transform animation,then they can be execute simultaneously.
In order to clarify the child position,we create a enum:
enum ItemOption {
POSITION_LEFT,
POSITION_RIGHT,
POSITION_TOP,
POSITION_BOTTOM,
POSITION_LEFT_TOP,
POSITION_RIGHT_TOP,
POSITION_LEFT_BOTTOM,
POSITION_RIGHT_BOTTOM,
}
each item relates a radians,below are relationships:
double tempAngle = 0;
switch (itemParameters.itemOption) {
case ItemOption.POSITION_LEFT:
tempAngle = 180;
break;
case ItemOption.POSITION_RIGHT:
tempAngle = 0;
break;
case ItemOption.POSITION_TOP:
tempAngle = 270;
break;
case ItemOption.POSITION_BOTTOM:
tempAngle = 90;
break;
case ItemOption.POSITION_LEFT_TOP:
tempAngle = 225;
break;
case ItemOption.POSITION_RIGHT_TOP:
tempAngle = 315;
break;
case ItemOption.POSITION_LEFT_BOTTOM:
tempAngle = 135;
break;
case ItemOption.POSITION_RIGHT_BOTTOM:
tempAngle = 45;
break;
}
and center icon in the center of icon.
(2)Center Icon
Center icon can be touched to execute opening and closing action,which corresponding to show and hide child items.
We decide to define two center icons in stack widget,and opening widget at top level,and closing widget placed below level,on open-widget clicking,it scale to zero with animation,and close-widget scale to one at the same way,within animation,the child items are spreading out and
moving back.
we define center icon:
var centerCloseWidget = Transform.scale(
scale: scale.value - 1,
// subtract the beginning value to run the opposite animation
child: FloatingActionButton(
child: centerCloseIcon,
onPressed: close,
backgroundColor: widget.centerIconBackground),
);
var centerOpenWidget = Transform.scale(
scale: scale.value,
child: FloatingActionButton(
child: centerOpenIcon,
backgroundColor: widget.centerIconBackground,
onPressed: open),
);
(3)Parent Widget
Finishing the below steps,we should design the top widget which Stack widget meets our requirements,its child widget are placed in the same position from bottom to top,otherwise including positioned widget.
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Stack(
alignment: widget.alignment,
children: getChildItemWidgets(itemDetails));
});
(4)Trailing
Designing widget interface is vital,
final ValueChanged<ItemOption> itemClicked;
final List<ItemParameters> itemParameterList;
final String centerIconOpenSourcePath;
final String centerIconCloseSourcePath;
final Color centerIconBackground;
final Alignment alignment;
const CircleMenuWidget({
@required this.itemParameterList,
this.itemClicked,
this.centerIconOpenSourcePath,
this.centerIconCloseSourcePath,
this.alignment = Alignment.center,
this.centerIconBackground = const Color(0xFFFF7404),
});
and ItemParameters define below:
class ItemParameters {
ItemOption itemOption;
String sourcePath;
double itemWidth;
double itemHeight;
ItemParameters(
this.itemOption, this.sourcePath, this.itemWidth, this.itemHeight);
}
itemClicked is responded to item clicked,itemParameterList include child item's position,image source path,width and height,centerIconOpenSourcePath,centerIconBackground and centerIconCloseSourcePath relate to center icon,alignment decide center-icon's position in screen.
3、How to use
List<String> path = [
"resources/images/home_task_cycle_normal.png",
"resources/images/home_task_run_normal.png",
"resources/images/home_task_walk_normal.png"
];
List<ItemParameters> itemParas = [];
ItemParameters itemParameters0 =
ItemParameters(ItemOption.POSITION_LEFT, path[0], 56, 56);
ItemParameters itemParameters1 =
ItemParameters(ItemOption.POSITION_LEFT_TOP, path[1], 56, 56);
ItemParameters itemParameters2 =
ItemParameters(ItemOption.POSITION_TOP, path[2], 56, 56);
itemParas.add(itemParameters0);
itemParas.add(itemParameters1);
itemParas.add(itemParameters2);
return Flexible(
child: Scaffold(
floatingActionButton: SizedBox.expand(
child: CircleMenuWidget(
itemParameterList: itemParas,
itemClicked: itemClicked,
alignment: Alignment.bottomRight,
)),
body: ListView(
children: <Widget>[...])
));
void itemClicked(ItemOption option) {
print("option desc ${option.index}");
}
One more thing
Simultaneously,i will paste the origin implements.
My rebuild code
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:vector_math/vector_math.dart' show radians;
class CircleMenuWidget extends StatefulWidget {
final ValueChanged<ItemOption> itemClicked;
final List<ItemParameters> itemParameterList;
final String centerIconOpenSourcePath;
final String centerIconCloseSourcePath;
final Color centerIconBackground;
final Alignment alignment;
const CircleMenuWidget({
@required this.itemParameterList,
this.itemClicked,
this.centerIconOpenSourcePath,
this.centerIconCloseSourcePath,
this.alignment = Alignment.center,
this.centerIconBackground = const Color(0xFFFF7404),
});
static _CircleMenuWidgetState curState;
@override
State<StatefulWidget> createState() {
curState = new _CircleMenuWidgetState();
return curState;
}
void open() {
curState.open();
}
void close() {
curState.close();
}
bool getStatus() {
return curState.closed;
}
}
class _CircleMenuWidgetState extends State<CircleMenuWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> scale;
Animation<double> translation;
Animation<double> rotation;
bool closed = true;
@override
void initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
translation = Tween<double>(
begin: 0.0,
end: 100.0,
).animate(
CurvedAnimation(parent: controller, curve: Curves.linear),
)..addListener(() {
setState(() {});
});
rotation = Tween<double>(
begin: 0.0,
end: 360.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.3,
0.9,
curve: Curves.decelerate,
),
),
)..addListener(() {
setState(() {});
});
scale = Tween<double>(
begin: 1,
end: 0.0,
).animate(
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn),
)..addListener(() {
setState(() {});
});
}
@override
Widget build(context) {
List<_ItemDetail> itemDetails = [];
if (widget.itemParameterList == null ||
widget.itemParameterList.length == 0) {
return Text("itemParameterList fail");
}
for (ItemParameters itemParameters in widget.itemParameterList) {
double width =
itemParameters.itemWidth == 0 ? 56 : itemParameters.itemWidth;
double height =
itemParameters.itemHeight == 0 ? 56 : itemParameters.itemHeight;
String sourcePath = itemParameters.sourcePath;
double tempAngle = 0;
switch (itemParameters.itemOption) {
case ItemOption.POSITION_LEFT:
tempAngle = 180;
break;
case ItemOption.POSITION_RIGHT:
tempAngle = 0;
break;
case ItemOption.POSITION_TOP:
tempAngle = 270;
break;
case ItemOption.POSITION_BOTTOM:
tempAngle = 90;
break;
case ItemOption.POSITION_LEFT_TOP:
tempAngle = 225;
break;
case ItemOption.POSITION_RIGHT_TOP:
tempAngle = 315;
break;
case ItemOption.POSITION_LEFT_BOTTOM:
tempAngle = 135;
break;
case ItemOption.POSITION_RIGHT_BOTTOM:
tempAngle = 45;
break;
}
_ItemDetail _itemDetail = new _ItemDetail(
tempAngle,
Image(
image: AssetImage(sourcePath),
width: width,
height: height,
),
itemParameters.itemOption);
itemDetails.add(_itemDetail);
}
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Stack(
alignment: widget.alignment,
children: getChildItemWidgets(itemDetails));
});
}
List<Widget> getChildItemWidgets(List<_ItemDetail> itemDetails) {
List<Widget> widgets = [];
for (_ItemDetail _itemDetail in itemDetails) {
final double rad = radians(_itemDetail.angle);
var childWidget = Transform(
transform: Matrix4.identity()
..translate(
(translation.value) * cos(rad), (translation.value) * sin(rad)),
child: Transform.rotate(
// Add rotation
angle: radians(rotation.value),
child: FloatingActionButton(
child: _itemDetail.childIcon,
onPressed: () {
widget.itemClicked(_itemDetail.itemOption);
},
elevation: 0)));
widgets.add(childWidget);
}
Widget centerOpenIcon;
if (widget.centerIconOpenSourcePath == null) {
centerOpenIcon = Icon(Icons.add);
} else {
centerOpenIcon =
Image(image: AssetImage(widget.centerIconOpenSourcePath));
}
Widget centerCloseIcon;
if (widget.centerIconCloseSourcePath == null) {
centerCloseIcon = Icon(Icons.close);
} else {
centerCloseIcon =
Image(image: AssetImage(widget.centerIconCloseSourcePath));
}
var centerCloseWidget = Transform.scale(
scale: scale.value - 1,
// subtract the beginning value to run the opposite animation
child: FloatingActionButton(
child: centerCloseIcon,
onPressed: close,
backgroundColor: widget.centerIconBackground),
);
var centerOpenWidget = Transform.scale(
scale: scale.value,
child: FloatingActionButton(
child: centerOpenIcon,
backgroundColor: widget.centerIconBackground,
onPressed: open),
);
widgets.add(centerCloseWidget);
widgets.add(centerOpenWidget);
return widgets;
}
open() {
closed = false;
controller.forward();
}
close() {
closed = true;
controller.reverse();
}
}
enum ItemOption {
POSITION_LEFT,
POSITION_RIGHT,
POSITION_TOP,
POSITION_BOTTOM,
POSITION_LEFT_TOP,
POSITION_RIGHT_TOP,
POSITION_LEFT_BOTTOM,
POSITION_RIGHT_BOTTOM,
}
class ItemParameters {
ItemOption itemOption;
String sourcePath;
double itemWidth;
double itemHeight;
ItemParameters(
this.itemOption, this.sourcePath, this.itemWidth, this.itemHeight);
}
class _ItemDetail {
double angle;
Widget childIcon;
ItemOption itemOption;
_ItemDetail(this.angle, this.childIcon, this.itemOption);
}
Origin implements
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:vector_math/vector_math.dart' show radians;
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void main() => runApp(MyApp());
// The parent Material App
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: SizedBox.expand(child: RadialAnimation())));
}
}
class RadialAnimation extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _RadiaAnimationState();
}
}
class _RadiaAnimationState extends State<RadialAnimation>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> scale;
Animation<double> translation;
Animation<double> rotation;
@override
void initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
translation = Tween<double>(
begin: 0.0,
end: 100.0,
).animate(
CurvedAnimation(parent: controller, curve: Curves.linear),
)..addListener(() {
setState(() {});
});
rotation = Tween<double>(
begin: 0.0,
end: 360.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.3,
0.9,
curve: Curves.decelerate,
),
),
)..addListener(() {
setState(() {});
});
scale = Tween<double>(
begin: 1,
end: 0.0,
).animate(
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn),
)..addListener(() {
setState(() {});
});
}
@override
Widget build(context) {
print(
"FloatCircleMenuView RadialAnimation build rotation:${rotation.value} and scale:${scale.value} and translate:${translation.value}");
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Transform.rotate(
// Add rotation
angle: radians(rotation.value),
child: Stack(alignment: Alignment.center, children: [
_buildButton(0,
color: Colors.red, icon: FontAwesomeIcons.thumbtack),
_buildButton(45,
color: Colors.green, icon: FontAwesomeIcons.sprayCan),
_buildButton(90,
color: Colors.orange, icon: FontAwesomeIcons.fire),
_buildButton(135,
color: Colors.blue, icon: FontAwesomeIcons.kiwiBird),
_buildButton(180,
color: Colors.black, icon: FontAwesomeIcons.cat),
_buildButton(225,
color: Colors.indigo, icon: FontAwesomeIcons.paw),
_buildButton(270,
color: Colors.pink, icon: FontAwesomeIcons.bong),
_buildButton(315,
color: Colors.yellow, icon: FontAwesomeIcons.bolt),
Transform.scale(
scale: scale.value - 1,
// subtract the beginning value to run the opposite animation
child: FloatingActionButton(
child: Icon(Icons.close),
onPressed: _close,
backgroundColor: Color(0xFFFF7404)),
),
Transform.scale(
scale: scale.value,
child: FloatingActionButton(
child: Icon(Icons.add),
backgroundColor: Color(0xFFFF7404),
onPressed: _open),
)
]),
);
});
}
_buildButton(double angle, {Color color, IconData icon}) {
final double rad = radians(angle);
return Transform(
transform: Matrix4.identity()
..translate(
(translation.value) * cos(rad), (translation.value) * sin(rad)),
child: FloatingActionButton(
child: Icon(icon),
backgroundColor: color,
onPressed: _openItemView(),
elevation: 0));
}
_open() {
controller.forward();
}
_close() {
controller.reverse();
}
_openItemView() {}
}
add font_awesome_flutter: ^8.4.0 to pubspec.yaml below dependencies.
Article will be synced to wechat blog:Android部落格
网友评论