https://docs.flutter.dev/cookbook/effects/expandable-fab
Create an expandable FAB
How to implement a FAB that expands to multiple buttons when tapped.
docs.flutter.dev
1. create an expandable fab
@immutable
class ExpandableFab extends StatefulWidget {
const ExpandableFab({
super.key,
this.initialOpen,
required this.distance,
required this.children,
});
final bool? initialOpen;
final double distance;
final List<Widget> children;
@override
_ExpandableFabState createState() => _ExpandableFabState();
}
class _ExpandableFabState extends State<ExpandableFab> {
@override
Widget build(BuildContext context) {
return const SizedBox();
}
}
initialOpen : 첫 open 여부
distance : floating actionbutton과 자식 widget 사이 거리
children : 자식 위젯 -> 펼쳤을 때 생기는 위젯
2. FAB cross-fade
class _ExpandableFabState extends State<ExpandableFab> {
bool _open = false;
@override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
}
void _toggle() {
setState(() {
_open = !_open;
});
}
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
_buildTapToCloseFab(),
_buildTapToOpenFab(),
],
),
);
}
Widget _buildTapToCloseFab() {
return SizedBox(
width: 56.0,
height: 56.0,
child: Center(
child: Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
elevation: 4.0,
child: InkWell(
onTap: _toggle,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.close,
color: Theme.of(context).primaryColor,
),
),
),
),
),
);
}
Widget _buildTapToOpenFab() {
return IgnorePointer(
ignoring: _open,
child: AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.diagonal3Values(
_open ? 0.7 : 1.0,
_open ? 0.7 : 1.0,
1.0,
),
duration: const Duration(milliseconds: 250),
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
child: AnimatedOpacity(
opacity: _open ? 0.0 : 1.0,
curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
duration: const Duration(milliseconds: 250),
child: FloatingActionButton(
onPressed: _toggle,
child: const Icon(Icons.create),
),
),
),
);
}
}
Stack에서 열기버튼은 닫기 버튼 위에 위치한다. 버튼을 클릭하여 나타나고 사라질 때 크로스 페이드의 시각적 모양을 허용한다. IgnorePointer를 통해서 위에 위치한 열기버튼이 사라졌을때 (_open = true일 때) 클릭을 막고 아래의 닫기 버튼을 클릭할 수 있도록 한다.
3. ActionButton 위젯 만들기
확장했을 때 생길 자식 버튼
class ActionButton extends StatelessWidget {
const ActionButton({
super.key,
this.onPressed,
required this.icon,
});
final VoidCallback? onPressed;
final Widget icon;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
color: theme.colorScheme.secondary,
elevation: 4.0,
child: IconButton(
onPressed: onPressed,
icon: icon,
color: theme.colorScheme.secondary,
),
);
}
}
4. ActionButton 확장 및 축소
ActionButton은 확장될 때 열린 FAB 아래에서 날아가야 하고 접힐 때 열린 FAB 아래로 날아가야 한다. AnimationController 및 Animation을 사용하여 다양한 ActionButton이 확장 및 축소되는 속도를 제어한다.
class _ExpandableFabState extends State<ExpandableFab>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _expandAnimation;
bool _open = false;
@override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
_controller = AnimationController(
value: _open ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
vsync: this,
);
_expandAnimation = CurvedAnimation(
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.easeOutQuad,
parent: _controller,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_open = !_open;
if (_open) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
}
ActionButton의 위치 조건은 아래와 같다.
1. degree (90도 안에)
2. 거리
3. 애니메이션
class _ExpandableActionButton extends StatelessWidget {
const _ExpandableActionButton(
{Key? key,
required this.distance,
required this.degree,
required this.progress,
required this.child})
: super(key: key);
final double distance;
final double degree;
final Animation<double> progress;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: progress,
builder: (BuildContext context, Widget? child) {
final Offset offset = Offset.fromDirection(degree * (pi / 180), progress.value * distance); //진행률을 distance에 곱해서 위치 지정
return Positioned(
right: offset.dx + 4,
bottom: offset.dy + 4,
child: Transform.rotate(
angle: (1.0 - progress.value) * pi / 2,
child: child!,
),
);
},
child: child);
}
}
degree, distance 그리고 애니메이션 진행률(progress.value)을 이용해서 Offset을 구한 다음 Positioned 위젯의 위치를 지정한다.
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
_buildTapToCloseFab(),
_buildTapToOpenFab(),
]..insertAll(0, _buildExpandableActionButton()),
),
);
}
List<_ExpandableActionButton> _buildExpandableActionButton() {
List<_ExpandableActionButton> animChildren = [];
final int count = widget.children.length;
final double gap = 90.0 / (count - 1);
for (var i = 0, degree = 0.0; i < count; i++, degree += gap) {
animChildren.add(_ExpandableActionButton(
distance: widget.distance,
progress: _expandAnimation,
degree: degree,
child: widget.children[i],
));
}
return animChildren;
}
'flutter' 카테고리의 다른 글
freezed live template로 쉽게 생성하기 (0) | 2022.08.11 |
---|---|
context 없이 navigation 구현하기 (0) | 2022.08.05 |
singleton (0) | 2022.06.14 |
flutter 해당 위젯 코드 쉽게 찾기 (0) | 2022.05.31 |
BoxConstraints forces an infinite width (0) | 2022.05.26 |