Flutter에서 FloatingActionButton을 어떻게 사용하나요?
질문
Flutter에서 FloatingActionButton의 사용 방법과 커스터마이징, 애니메이션 적용 등에 대해 자세히 설명해주세요.
답변
Flutter의 FloatingActionButton
(FAB)은 Material Design 가이드라인에 따라 앱의 주요 기능을 강조하기 위한 둥근 버튼으로, 일반적으로 화면 우측 하단에 위치합니다. 이 버튼은 사용자가 앱의 주요 기능에 빠르게 접근할 수 있게 해줍니다.
1. 기본 FloatingActionButton 사용법
FloatingActionButton
은 주로 Scaffold
위젯의 floatingActionButton
속성으로 설정합니다.
import 'package:flutter/material.dart';
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FloatingActionButton 예제'),
),
body: Center(
child: Text('FloatingActionButton 사용 예제'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 버튼이 눌렸을 때 실행할 코드
print('FAB가 눌렸습니다!');
},
child: Icon(Icons.add),
tooltip: '새 항목 추가',
),
);
}
}
1.1 FloatingActionButton의 위치 조정
Scaffold
의 floatingActionButtonLocation
속성을 사용하여 FAB의 위치를 조정할 수 있습니다.
Scaffold(
// ... 다른 속성들
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
)
사용 가능한 위치 옵션:
FloatingActionButtonLocation.endFloat
(기본값, 우측 하단에 떠 있음)FloatingActionButtonLocation.centerFloat
(중앙 하단에 떠 있음)FloatingActionButtonLocation.startFloat
(좌측 하단에 떠 있음)FloatingActionButtonLocation.endDocked
(우측 하단에 도킹됨)FloatingActionButtonLocation.centerDocked
(중앙 하단에 도킹됨)FloatingActionButtonLocation.startDocked
(좌측 하단에 도킹됨)
1.2 다양한 크기의 FloatingActionButton
Flutter는 세 가지 크기의 FAB를 제공합니다:
// 기본 크기 FAB (56x56dp)
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
)
// 작은 크기 FAB (40x40dp)
FloatingActionButton.small(
onPressed: () {},
child: Icon(Icons.add),
)
// 확장된 FAB (와이드 버튼)
FloatingActionButton.extended(
onPressed: () {},
label: Text('새 항목 추가'),
icon: Icon(Icons.add),
)
2. FloatingActionButton 커스터마이징
2.1 스타일 및 색상 커스터마이징
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
backgroundColor: Colors.red, // 배경색 변경
foregroundColor: Colors.white, // 아이콘 색상 변경
elevation: 12.0, // 그림자 높이 조정
highlightElevation: 20.0, // 눌렸을 때 그림자 높이
shape: RoundedRectangleBorder( // 모양 변경
borderRadius: BorderRadius.circular(16.0),
),
mini: true, // small과 동일한 효과 (작은 FAB)
)
2.2 FloatingActionButton에 Hero 애니메이션 적용하기
화면 전환 시 부드러운 애니메이션을 위해 Hero 위젯을 사용할 수 있습니다.
// 첫 번째 화면
Scaffold(
floatingActionButton: Hero(
tag: 'fab-hero',
child: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => DetailScreen()),
);
},
child: Icon(Icons.add),
),
),
)
// 두 번째 화면 (상세 화면)
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('상세 화면')),
body: Center(child: Text('상세 내용')),
floatingActionButton: Hero(
tag: 'fab-hero',
child: FloatingActionButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Icon(Icons.close),
),
),
);
}
}
2.3 커스텀 모양과 애니메이션
특별한 모양이나 애니메이션을 적용하려면 FAB를 직접 구현할 수 있습니다.
import 'dart:math' as math;
class AnimatedFab extends StatefulWidget {
final VoidCallback onPressed;
final String tooltip;
final IconData icon;
AnimatedFab({required this.onPressed, required this.tooltip, required this.icon});
@override
_AnimatedFabState createState() => _AnimatedFabState();
}
class _AnimatedFabState extends State<AnimatedFab>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: FloatingActionButton(
onPressed: () {
if (_controller.isDismissed) {
_controller.forward();
} else {
_controller.reverse();
}
widget.onPressed();
},
tooltip: widget.tooltip,
child: Icon(widget.icon),
),
);
},
);
}
}
// 사용 예시
Scaffold(
floatingActionButton: AnimatedFab(
onPressed: () {},
tooltip: '회전하는 FAB',
icon: Icons.add,
),
)
3. 여러 개의 FloatingActionButton 구현하기
3.1 스피드 다이얼 패턴
여러 액션을 처리하기 위해 확장되는 FAB 메뉴를 구현할 수 있습니다.
import 'package:flutter/material.dart';
class SpeedDialFab extends StatefulWidget {
@override
_SpeedDialFabState createState() => _SpeedDialFabState();
}
class _SpeedDialFabState extends State<SpeedDialFab>
with SingleTickerProviderStateMixin {
bool _isOpen = false;
late AnimationController _animationController;
late Animation<double> _animateIcon;
late Animation<double> _translateButton;
final Curve _curve = Curves.easeOut;
final double _fabHeight = 56.0;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
)..addListener(() {
setState(() {});
});
_animateIcon =
Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
_translateButton = Tween<double>(
begin: _fabHeight,
end: -14.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.0,
0.75,
curve: _curve,
),
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Widget _buildFabMenuItem(IconData icon, String title, VoidCallback onTap) {
return Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value * 2.0,
0.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 버튼 레이블
Container(
margin: EdgeInsets.only(right: 10),
child: Material(
color: Colors.blue,
elevation: 4,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
title,
style: TextStyle(color: Colors.white),
),
),
),
),
// 버튼
FloatingActionButton(
onPressed: onTap,
mini: true,
child: Icon(icon),
backgroundColor: Colors.blue,
),
],
),
);
}
Widget _buildFabMenuItem2(IconData icon, String title, VoidCallback onTap) {
return Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value * 3.0,
0.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
margin: EdgeInsets.only(right: 10),
child: Material(
color: Colors.green,
elevation: 4,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
title,
style: TextStyle(color: Colors.white),
),
),
),
),
FloatingActionButton(
onPressed: onTap,
mini: true,
child: Icon(icon),
backgroundColor: Colors.green,
),
],
),
);
}
Widget _buildFabMenuItem3(IconData icon, String title, VoidCallback onTap) {
return Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value * 4.0,
0.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
margin: EdgeInsets.only(right: 10),
child: Material(
color: Colors.red,
elevation: 4,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
title,
style: TextStyle(color: Colors.white),
),
),
),
),
FloatingActionButton(
onPressed: onTap,
mini: true,
child: Icon(icon),
backgroundColor: Colors.red,
),
],
),
);
}
void _toggle() {
if (_isOpen) {
_animationController.reverse();
} else {
_animationController.forward();
}
_isOpen = !_isOpen;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Speed Dial FAB'),
),
body: Center(
child: Text('스피드 다이얼 FAB 예제'),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildFabMenuItem3(
Icons.camera_alt,
'사진 촬영',
() => print('사진 촬영'),
),
_buildFabMenuItem2(
Icons.photo,
'사진 선택',
() => print('사진 선택'),
),
_buildFabMenuItem(
Icons.note_add,
'메모 추가',
() => print('메모 추가'),
),
// 메인 FAB
FloatingActionButton(
child: AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: _animateIcon,
),
onPressed: _toggle,
backgroundColor: Colors.deepPurple,
),
],
),
);
}
}
3.2 Flutter 패키지 사용하기
복잡한 구현 대신 패키지를 사용할 수 있습니다.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_speed_dial: ^6.0.0
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
Scaffold(
floatingActionButton: SpeedDial(
animatedIcon: AnimatedIcons.menu_close,
animatedIconTheme: IconThemeData(size: 22.0),
curve: Curves.bounceIn,
overlayColor: Colors.black,
overlayOpacity: 0.5,
children: [
SpeedDialChild(
child: Icon(Icons.add),
backgroundColor: Colors.blue,
label: '새 항목',
labelStyle: TextStyle(fontSize: 18.0),
onTap: () => print('새 항목 추가'),
),
SpeedDialChild(
child: Icon(Icons.edit),
backgroundColor: Colors.green,
label: '편집',
labelStyle: TextStyle(fontSize: 18.0),
onTap: () => print('편집 모드'),
),
SpeedDialChild(
child: Icon(Icons.delete),
backgroundColor: Colors.red,
label: '삭제',
labelStyle: TextStyle(fontSize: 18.0),
onTap: () => print('삭제 모드'),
),
],
),
)
4. BottomAppBar와 함께 사용하기
FAB는 BottomAppBar
와 결합하여 더 통합된 UI를 제공할 수 있습니다.
Scaffold(
appBar: AppBar(
title: Text('FAB와 BottomAppBar'),
),
body: Center(
child: Text('FAB와 BottomAppBar 예제'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
shape: CircularNotchedRectangle(), // FAB를 위한 노치(오목한 부분) 생성
notchMargin: 8.0, // 노치 마진 설정
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
IconButton(
icon: Icon(Icons.home),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
SizedBox(width: 48), // FAB를 위한 공간
IconButton(
icon: Icon(Icons.favorite),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.person),
onPressed: () {},
),
],
),
),
)
5. 스크롤에 반응하는 FAB
사용자가 스크롤할 때 FAB를 자동으로 숨기거나 표시할 수 있습니다.
class ScrollableFabExample extends StatefulWidget {
@override
_ScrollableFabExampleState createState() => _ScrollableFabExampleState();
}
class _ScrollableFabExampleState extends State<ScrollableFabExample> {
bool _isVisible = true;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.userScrollDirection == ScrollDirection.reverse) {
if (_isVisible) {
setState(() {
_isVisible = false;
});
}
} else if (_scrollController.position.userScrollDirection == ScrollDirection.forward) {
if (!_isVisible) {
setState(() {
_isVisible = true;
});
}
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('스크롤에 반응하는 FAB'),
),
body: ListView.builder(
controller: _scrollController,
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(
title: Text('항목 $index'),
);
},
),
floatingActionButton: AnimatedSlide(
duration: Duration(milliseconds: 300),
offset: _isVisible ? Offset.zero : Offset(0, 2),
child: AnimatedOpacity(
duration: Duration(milliseconds: 300),
opacity: _isVisible ? 1.0 : 0.0,
child: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
),
),
);
}
}
6. 상태에 따라 FAB 변경하기
앱의 상태에 따라 FAB의 색상, 아이콘, 동작을 변경할 수 있습니다.
class StatefulFabExample extends StatefulWidget {
@override
_StatefulFabExampleState createState() => _StatefulFabExampleState();
}
class _StatefulFabExampleState extends State<StatefulFabExample> {
bool _isEditing = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('상태에 따라 변하는 FAB'),
),
body: Center(
child: Text(_isEditing ? '편집 모드' : '보기 모드'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_isEditing = !_isEditing;
});
},
backgroundColor: _isEditing ? Colors.red : Colors.blue,
child: Icon(_isEditing ? Icons.check : Icons.edit),
tooltip: _isEditing ? '저장하기' : '편집하기',
),
);
}
}
7. FAB 반응형 디자인
화면 크기에 따라 FAB의 크기나 종류를 조정할 수 있습니다.
class ResponsiveFab extends StatelessWidget {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final bool isLargeScreen = size.width > 600;
return Scaffold(
appBar: AppBar(
title: Text('반응형 FAB'),
),
body: Center(
child: Text('화면 크기에 따라 FAB가 변합니다'),
),
floatingActionButton: isLargeScreen
? FloatingActionButton.extended(
onPressed: () {},
icon: Icon(Icons.add),
label: Text('새 항목 추가'),
)
: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
);
}
}
8. 실제 프로젝트에서의 FAB 활용 예시
8.1 할 일 앱에서 새 항목 추가하기
class TodoApp extends StatefulWidget {
@override
_TodoAppState createState() => _TodoAppState();
}
class _TodoAppState extends State<TodoApp> {
final List<String> _todos = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('할 일 목록'),
),
body: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_todos[index]),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_showAddTodoDialog(context);
},
child: Icon(Icons.add),
tooltip: '할 일 추가',
),
);
}
void _showAddTodoDialog(BuildContext context) {
final TextEditingController controller = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('할 일 추가'),
content: TextField(
controller: controller,
decoration: InputDecoration(
labelText: '할 일',
hintText: '새로운 할 일을 입력하세요',
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('취소'),
),
TextButton(
onPressed: () {
if (controller.text.isNotEmpty) {
setState(() {
_todos.add(controller.text);
});
Navigator.of(context).pop();
}
},
child: Text('추가'),
),
],
);
},
);
}
}
8.2 카메라 앱에서 다양한 옵션 제공하기
class CameraApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('카메라 앱'),
),
body: Center(
child: Text('카메라 미리보기'),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
// 갤러리에서 이미지 선택
print('갤러리에서 선택');
},
heroTag: 'gallery',
mini: true,
child: Icon(Icons.photo_library),
backgroundColor: Colors.green,
),
SizedBox(height: 16),
FloatingActionButton.large(
onPressed: () {
// 사진 촬영
print('사진 촬영');
},
heroTag: 'camera',
child: Icon(Icons.camera_alt, size: 36),
backgroundColor: Colors.red,
),
SizedBox(height: 16),
FloatingActionButton(
onPressed: () {
// 카메라 설정
print('카메라 설정');
},
heroTag: 'settings',
mini: true,
child: Icon(Icons.settings),
backgroundColor: Colors.blue,
),
],
),
);
}
}
9. FloatingActionButton의 접근성 개선
FAB를 사용할 때 접근성을 고려하는 것이 중요합니다.
FloatingActionButton(
onPressed: () {
// 작업 수행
},
child: Icon(Icons.add),
tooltip: '새 항목 추가', // 스크린 리더를 위한 설명
semanticLabel: '새로운 항목을 목록에 추가합니다', // 더 자세한 접근성 레이블
)
요약
Flutter의 FloatingActionButton
은 앱의 주요 기능에 빠르게 접근할 수 있는 중요한 UI 요소입니다. 기본 사용법은 간단하지만, 다양한 방법으로 커스터마이징하고 확장할 수 있습니다.
- 기본 사용법:
Scaffold
의floatingActionButton
속성을 통해 쉽게 구현 - 위치 조정:
floatingActionButtonLocation
을 통해 화면 내 위치 조정 - 다양한 크기: 기본, 작은 크기, 확장된 형태 지원
- 커스터마이징: 색상, 그림자, 모양, 애니메이션 등 다양한 속성 조정 가능
- 스피드 다이얼: 여러 액션을 그룹화하여 확장되는 메뉴로 구현 가능
- BottomAppBar 통합: 앱 바와 통합하여 일관된 디자인 구현
- 스크롤 반응형: 사용자의 스크롤에 따라 동적으로 표시/숨김 가능
- 상태 반응형: 앱 상태에 따라 색상, 아이콘, 동작 변경 가능
- 반응형 디자인: 화면 크기에 따라 적절한 형태로 변경 가능
- 접근성 고려: 툴팁과 시맨틱 레이블을 통한 접근성 개선
FloatingActionButton
을 효과적으로 활용하여 사용자가 앱의 주요 기능에 빠르게 접근할 수 있도록 하고, 시각적으로도 매력적인 UI를 구현할 수 있습니다.