메인 화면에 FloatingActionButton을 이용해 "알람 추가" 기능을 구현 했는데 앱에 다른 기능이 추가 될 경우를 생각해 확장성이 더 좋다고 생각되는 Flow위젯을 쓰게 됐다.
class FloatingFlowButton extends StatefulWidget{
const FloatingFlowButton({Key? key}) : super(key: key);
@override
_FloatingFlowButtonState createState() => _FloatingFlowButtonState();
}
class _FloatingFlowButtonState extends State<FloatingFlowButton> with TickerProviderStateMixin{
late AnimationController menuAnimation;
final menuItems = <IconData>[
Icons.home,
Icons.new_releases,
Icons.notifications,
Icons.settings,
Icons.menu,
];
@override
Widget build(BuildContext context) {
// TODO: implement build
return Flow(
delegate: FloatingFlowButtonDelegate(),
children: menuItems.map<Widget>((IconData iconData) => buildChild(iconData)).toList(),
);
}
}
class FloatingFlowButtonDelegate extends FlowDelegate{
final Animation<double> menuAnimation;
FloatingFlowButtonDelegate({required this.menuAnimation});
@override
void paintChildren(FlowPaintingContext context) {
// TODO: implement paintChildren
}
@override
bool shouldRepaint(covariant FlowDelegate oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
별도의 클래스를 만들고 FlowDelegate를 상속받는다.
@override
void initState() {
super.initState();
menuAnimation = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this
);
}
menuAnimation변수를 생성하고 initState()에서 초기화 한다.
Widget buildChild(IconData icon){
return Container(
child: RawMaterialButton(
fillColor: Colors.blue,
shape: const CircleBorder(),
constraints: BoxConstraints.tight(Size(50,50)),
onPressed: (){
print('onpress ${menuAnimation.status}');
menuAnimation.status == AnimationStatus.dismissed
? menuAnimation.forward()
: menuAnimation.reverse();
},
child: Icon(
icon,
size: 20,
),
),
);
}
constraints속성에 주는 값이 각각의 버튼의 크기이다.
공식 문서나 다른 글에서 AnimationStatus.completed로 되어 있는 경우가 있는데 초기 상태를 찍어보니 dismissed여서 dismissed로 설정 했다. 작동하지 않는다면 상태를 확인해보고 바꾸면 된다.
@override
void paintChildren(FlowPaintingContext context) {
double x = context.size.width - context.getChildSize(0)!.width;
double y = context.size.height - context.getChildSize(0)!.height;
for(int i = context.childCount-1 ; i>=0; i--){
Size? buttonSize = context.getChildSize(i);
double dx = x;
double dy = buttonSize!.height * i;
context.paintChild(
i,
transform: Matrix4.translationValues(
dx,
y - dy*menuAnimation.value,
0
)
);
}
}
FloatingFlowButtonDelegate의 paintChildren()메소드
처음 2줄이 중요한데 Flow위젯은
Positioned(
bottom: 0,
right: 0,
child: FloatingFlowButton()
)
이런식으로 사용하면 에러가난다. paintChildren에서 위젯의 위치를 지정해야한다.
그 다음 for문이 ++이 아닌 --인데 이유는 Flow의 children에 들어가는 아이콘들이 그려지는 순서대로 쌓이기 때문이다. 그렇기 때문에 첫 번째 아이콘을 마지막으로 그리도록 해야해서 역순이다.
여기서 한번 정리하고 가자면
1. paintChildren안에서 세부 위치를 지정해야 한다.
2. 그려지는 순서대로 위젯이 쌓이게 된다.
@override
bool shouldRepaint(FloatingFlowButtonDelegate oldDelegate) {
return menuAnimation != oldDelegate.menuAnimation;
}
마지막으로 shouldRepaint 구현
자동 완성으로 메소드를 만들면 메소드의 인자가 FlowDelegate이라서 oldDelegate에 menuAnimation이 없기 때문에 바꿔줘야한다.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class FloatingFlowButton extends StatefulWidget{
const FloatingFlowButton({Key? key}) : super(key: key);
@override
_FloatingFlowButtonState createState() => _FloatingFlowButtonState();
}
class _FloatingFlowButtonState extends State<FloatingFlowButton> with TickerProviderStateMixin{
late AnimationController menuAnimation;
final menuItems = <IconData>[
Icons.home,
Icons.notifications,
Icons.settings,
Icons.menu,
];
@override
void initState() {
super.initState();
menuAnimation = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this
);
}
Widget buildChild(IconData icon){
final widgetSizePerWidth = MediaQuery.of(context).size.width/menuItems.length;
return Container(
// color: Colors.black,
child: RawMaterialButton(
fillColor: Colors.blue,
shape: const CircleBorder(),
// constraints: BoxConstraints.tight(Size(widgetSizePerWidth,widgetSizePerWidth)),
constraints: BoxConstraints.tight(Size(50,50)),
onPressed: (){
print('onpress ${menuAnimation.status}');
menuAnimation.status == AnimationStatus.dismissed
? menuAnimation.forward()
: menuAnimation.reverse();
},
child: Icon(
icon,
size: 20,
),
),
);
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Flow(
delegate: FloatingFlowButtonDelegate(menuAnimation: menuAnimation),
children: menuItems.map<Widget>((IconData iconData) => buildChild(iconData)).toList(),
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
menuAnimation.dispose();
}
}
class FloatingFlowButtonDelegate extends FlowDelegate{
final Animation<double> menuAnimation;
FloatingFlowButtonDelegate({required this.menuAnimation}):super(repaint: menuAnimation);
@override
void paintChildren(FlowPaintingContext context) {
double x = context.size.width - context.getChildSize(0)!.width;
double y = context.size.height - context.getChildSize(0)!.height;
// for(int i = 0; i<context.childCount; i++){
for(int i = context.childCount-1 ; i>=0; i--){
Size? buttonSize = context.getChildSize(i);
double dx = x;
double dy = buttonSize!.height * i;
context.paintChild(
i,
transform: Matrix4.translationValues(
dx,
y - dy*menuAnimation.value,
0
)
);
}
}
@override
bool shouldRepaint(FloatingFlowButtonDelegate oldDelegate) {
return menuAnimation != oldDelegate.menuAnimation;
}
}