이번에는 floatingActionButton 을 확장하는 ExpandableFab 기능을 구현해보겠습니다.
개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1
화면 구성은 아래와 같습니다.
./src/screens/home/home_screen.dart - Scaffold 하위에 floatingActionButton 관련 추가
floatingActionButton: ExpandableFab(
// distance between button and children,
distance: 90,
children: <Widget>[
MaterialButton(
onPressed: () {},
shape: const CircleBorder(),
height: 48,
color: Theme.of(context).colorScheme.primary,
child: const Icon(Icons.edit),
),
MaterialButton(
onPressed: () {},
shape: const CircleBorder(),
height: 48,
color: Theme.of(context).colorScheme.primary,
child: const Icon(Icons.input),
),
MaterialButton(
onPressed: () {},
shape: const CircleBorder(),
height: 48,
color: Theme.of(context).colorScheme.primary,
child: const Icon(Icons.add),
),
],
),
./src/widgets/expandable_fab.dart
import 'package:flutter/material.dart';
import 'dart:math';
@immutable
class ExpandableFab extends StatefulWidget {
const ExpandableFab({Key? key, this.initialOpen, required this.distance, required this.children})
: super(key: key);
final bool? initialOpen;
final double distance;
final List<Widget> children;
static const Duration duration = Duration(milliseconds: 300);
@override
_ExpandableFabState createState() => _ExpandableFabState();
}
class _ExpandableFabState extends State<ExpandableFab> with SingleTickerProviderStateMixin {
bool _open = false;
late AnimationController _controller;
Animation<double>? _expandAnimation;
@override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
_controller = AnimationController(
value: _open ? 1 : 0.0,
duration: ExpandableFab.duration,
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();
}
});
}
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
SizedBox( // 커스텀 위치/사이즈 조정
height: 56,
width: 56,
child: Center(child: _buildTapToCloseFab()),
),
_buildTapToOpenFab(),
]..insertAll(1, _buildExpandingActionButtons()),
),
);
}
List<Widget> _buildExpandingActionButtons() {
final children = <Widget>[];
final count = widget.children.length;
final step = 90.0 / (count - 1);
for (var i = 0, angleInDegrees = 0; i < count; i++, angleInDegrees += step.toInt()) {
children.add(
_ExpandingActionButton(
directionInDegrees: angleInDegrees.toDouble(),
maxDistance: widget.distance,
progress: _expandAnimation,
child: widget.children[i],
),
);
}
return children;
}
Widget _buildTapToCloseFab() {
return AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.rotationZ(_open ? 0 : pi / 4),
duration: ExpandableFab.duration,
curve: Curves.easeOut,
child: FloatingActionButton(
heroTag: 'btn1',
onPressed: _toggle,
mini: true,
backgroundColor: Colors.white,
child: Icon(
Icons.close,
color: Theme.of(context).primaryColor,
),
),
);
}
Widget _buildTapToOpenFab() {
return IgnorePointer(
ignoring: _open,
child: AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.rotationZ(_open ? 0 : pi / 4),
duration: ExpandableFab.duration,
curve: Curves.easeOut,
child: AnimatedOpacity(
opacity: _open ? 0.0 : 1.0,
duration: ExpandableFab.duration,
child: FloatingActionButton(
heroTag: 'btn2',
onPressed: _toggle,
child: const Icon(Icons.close),
),
),
),
);
}
}
@immutable
class _ExpandingActionButton extends StatelessWidget {
const _ExpandingActionButton({
Key? key,
required this.directionInDegrees,
required this.maxDistance,
required this.progress,
required this.child,
}) : super(key: key);
final double directionInDegrees;
final double maxDistance;
final Animation<double>? progress;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: progress!,
builder: (BuildContext context, Widget? child) {
final offset = Offset.fromDirection(
directionInDegrees * (pi / 180.0),
progress!.value * maxDistance,
);
return Positioned(
// 커스텀 위치 조정
right: offset.dx - 16,
bottom: offset.dy,
child: Transform.rotate(
angle: (1.0 - progress!.value) * pi / 2,
// 커스텀 사이즈 조정, 56은 일반적인 FloatingActionButton 사이즈임,
// 전달된 위젯을 사이즈를 사전에 조정 가능함,
child: SizedBox(height: 56, child: Center(child: child)),
),
);
},
child: FadeTransition(
opacity: progress!,
child: child,
),
);
}
}
'Flutter > 12 Clone 'Used Goods app'' 카테고리의 다른 글
[Flutter] Clone - 당근마켓25(InputScreen) (0) | 2022.08.08 |
---|---|
[Flutter] Clone - 당근마켓24(fixing Getx router error) (0) | 2022.08.05 |
[Flutter] Clone - 당근마켓22(userModel 구현) (0) | 2022.08.03 |
[Flutter] Clone - 당근마켓21(userModel) (0) | 2022.08.03 |
[Flutter] Clone - 당근마켓20(Firestore database) (0) | 2022.08.03 |