Encrypted messaging app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
5.0 KiB

  1. import 'dart:math' as math;
  2. import 'package:flutter/material.dart';
  3. class ExpandableFab extends StatefulWidget {
  4. const ExpandableFab({
  5. Key? key,
  6. this.initialOpen,
  7. required this.distance,
  8. required this.icon,
  9. required this.children,
  10. }) : super(key: key);
  11. final bool? initialOpen;
  12. final double distance;
  13. final Icon icon;
  14. final List<Widget> children;
  15. @override
  16. State<ExpandableFab> createState() => _ExpandableFabState();
  17. }
  18. class _ExpandableFabState extends State<ExpandableFab>
  19. with SingleTickerProviderStateMixin {
  20. late final AnimationController _controller;
  21. late final Animation<double> _expandAnimation;
  22. bool _open = false;
  23. @override
  24. void initState() {
  25. super.initState();
  26. _open = widget.initialOpen ?? false;
  27. _controller = AnimationController(
  28. value: _open ? 1.0 : 0.0,
  29. duration: const Duration(milliseconds: 250),
  30. vsync: this,
  31. );
  32. _expandAnimation = CurvedAnimation(
  33. curve: Curves.fastOutSlowIn,
  34. reverseCurve: Curves.easeOutQuad,
  35. parent: _controller,
  36. );
  37. }
  38. @override
  39. void dispose() {
  40. _controller.dispose();
  41. super.dispose();
  42. }
  43. void _toggle() {
  44. setState(() {
  45. _open = !_open;
  46. if (_open) {
  47. _controller.forward();
  48. } else {
  49. _controller.reverse();
  50. }
  51. });
  52. }
  53. @override
  54. Widget build(BuildContext context) {
  55. return SizedBox.expand(
  56. child: Stack(
  57. alignment: Alignment.bottomRight,
  58. clipBehavior: Clip.none,
  59. children: [
  60. _buildTapToCloseFab(),
  61. ..._buildExpandingActionButtons(),
  62. _buildTapToOpenFab(),
  63. ],
  64. ),
  65. );
  66. }
  67. Widget _buildTapToCloseFab() {
  68. return SizedBox(
  69. width: 56.0,
  70. height: 56.0,
  71. child: Center(
  72. child: Material(
  73. shape: const CircleBorder(),
  74. clipBehavior: Clip.antiAlias,
  75. elevation: 4.0,
  76. child: InkWell(
  77. onTap: _toggle,
  78. child: Padding(
  79. padding: const EdgeInsets.all(8.0),
  80. child: Icon(
  81. Icons.close,
  82. color: Theme.of(context).primaryColor,
  83. ),
  84. ),
  85. ),
  86. ),
  87. ),
  88. );
  89. }
  90. List<Widget> _buildExpandingActionButtons() {
  91. final children = <Widget>[];
  92. final count = widget.children.length;
  93. final step = 60.0 / (count - 1);
  94. for (var i = 0, angleInDegrees = 15.0;
  95. i < count;
  96. i++, angleInDegrees += step) {
  97. children.add(
  98. _ExpandingActionButton(
  99. directionInDegrees: angleInDegrees,
  100. maxDistance: widget.distance,
  101. progress: _expandAnimation,
  102. child: widget.children[i],
  103. ),
  104. );
  105. }
  106. return children;
  107. }
  108. Widget _buildTapToOpenFab() {
  109. return IgnorePointer(
  110. ignoring: _open,
  111. child: AnimatedContainer(
  112. transformAlignment: Alignment.center,
  113. transform: Matrix4.diagonal3Values(
  114. _open ? 0.7 : 1.0,
  115. _open ? 0.7 : 1.0,
  116. 1.0,
  117. ),
  118. duration: const Duration(milliseconds: 250),
  119. curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
  120. child: AnimatedOpacity(
  121. opacity: _open ? 0.0 : 1.0,
  122. curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
  123. duration: const Duration(milliseconds: 250),
  124. child: FloatingActionButton(
  125. onPressed: _toggle,
  126. backgroundColor: Theme.of(context).colorScheme.primary,
  127. child: widget.icon,
  128. ),
  129. ),
  130. ),
  131. );
  132. }
  133. }
  134. @immutable
  135. class _ExpandingActionButton extends StatelessWidget {
  136. const _ExpandingActionButton({
  137. required this.directionInDegrees,
  138. required this.maxDistance,
  139. required this.progress,
  140. required this.child,
  141. });
  142. final double directionInDegrees;
  143. final double maxDistance;
  144. final Animation<double> progress;
  145. final Widget child;
  146. @override
  147. Widget build(BuildContext context) {
  148. return AnimatedBuilder(
  149. animation: progress,
  150. builder: (context, child) {
  151. final offset = Offset.fromDirection(
  152. directionInDegrees * (math.pi / 180.0),
  153. progress.value * maxDistance,
  154. );
  155. return Positioned(
  156. right: 4.0 + offset.dx,
  157. bottom: 4.0 + offset.dy,
  158. child: Transform.rotate(
  159. angle: (1.0 - progress.value) * math.pi / 2,
  160. child: child!,
  161. ),
  162. );
  163. },
  164. child: FadeTransition(
  165. opacity: progress,
  166. child: child,
  167. ),
  168. );
  169. }
  170. }
  171. class ActionButton extends StatelessWidget {
  172. const ActionButton({
  173. Key? key,
  174. this.onPressed,
  175. required this.icon,
  176. }) : super(key: key);
  177. final VoidCallback? onPressed;
  178. final Widget icon;
  179. @override
  180. Widget build(BuildContext context) {
  181. final theme = Theme.of(context);
  182. return Material(
  183. shape: const CircleBorder(),
  184. clipBehavior: Clip.antiAlias,
  185. color: theme.colorScheme.secondary,
  186. elevation: 4.0,
  187. child: IconButton(
  188. onPressed: onPressed,
  189. icon: icon,
  190. color: theme.colorScheme.onSecondary,
  191. ),
  192. );
  193. }
  194. }