import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
class ExpandableFab extends StatefulWidget {
|
|
const ExpandableFab({
|
|
Key? key,
|
|
this.initialOpen,
|
|
required this.distance,
|
|
required this.icon,
|
|
required this.children,
|
|
}) : super(key: key);
|
|
|
|
final bool? initialOpen;
|
|
final double distance;
|
|
final Icon icon;
|
|
final List<Widget> children;
|
|
|
|
@override
|
|
State<ExpandableFab> createState() => _ExpandableFabState();
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox.expand(
|
|
child: Stack(
|
|
alignment: Alignment.bottomRight,
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
_buildTapToCloseFab(),
|
|
..._buildExpandingActionButtons(),
|
|
_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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildExpandingActionButtons() {
|
|
final children = <Widget>[];
|
|
final count = widget.children.length;
|
|
final step = 60.0 / (count - 1);
|
|
for (var i = 0, angleInDegrees = 15.0;
|
|
i < count;
|
|
i++, angleInDegrees += step) {
|
|
children.add(
|
|
_ExpandingActionButton(
|
|
directionInDegrees: angleInDegrees,
|
|
maxDistance: widget.distance,
|
|
progress: _expandAnimation,
|
|
child: widget.children[i],
|
|
),
|
|
);
|
|
}
|
|
return children;
|
|
}
|
|
|
|
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,
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
child: widget.icon,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@immutable
|
|
class _ExpandingActionButton extends StatelessWidget {
|
|
const _ExpandingActionButton({
|
|
required this.directionInDegrees,
|
|
required this.maxDistance,
|
|
required this.progress,
|
|
required this.child,
|
|
});
|
|
|
|
final double directionInDegrees;
|
|
final double maxDistance;
|
|
final Animation<double> progress;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: progress,
|
|
builder: (context, child) {
|
|
final offset = Offset.fromDirection(
|
|
directionInDegrees * (math.pi / 180.0),
|
|
progress.value * maxDistance,
|
|
);
|
|
return Positioned(
|
|
right: 4.0 + offset.dx,
|
|
bottom: 4.0 + offset.dy,
|
|
child: Transform.rotate(
|
|
angle: (1.0 - progress.value) * math.pi / 2,
|
|
child: child!,
|
|
),
|
|
);
|
|
},
|
|
child: FadeTransition(
|
|
opacity: progress,
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ActionButton extends StatelessWidget {
|
|
const ActionButton({
|
|
Key? key,
|
|
this.onPressed,
|
|
required this.icon,
|
|
}) : super(key: key);
|
|
|
|
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.onSecondary,
|
|
),
|
|
);
|
|
}
|
|
}
|