You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
14 KiB
Dart
373 lines
14 KiB
Dart
|
3 months ago
|
import 'package:flutter/material.dart';
|
||
|
|
|
||
|
|
import 'package:uco_mobile_poc/app/res/app_colors.dart';
|
||
|
|
|
||
|
|
class SpeedDial extends StatefulWidget {
|
||
|
|
const SpeedDial({
|
||
|
|
Key? key,
|
||
|
|
required this.child,
|
||
|
|
required this.speedDialChildren,
|
||
|
|
this.labelsStyle,
|
||
|
|
this.labelsBackgroundColor,
|
||
|
|
this.controller,
|
||
|
|
this.closedForegroundColor,
|
||
|
|
this.openForegroundColor,
|
||
|
|
this.closedBackgroundColor,
|
||
|
|
this.openBackgroundColor,
|
||
|
|
}) : super(key: key);
|
||
|
|
|
||
|
|
final Widget child;
|
||
|
|
|
||
|
|
/// A list of [SpeedDialChild] to display when the [SpeedDial] is open.
|
||
|
|
final List<SpeedDialChild> speedDialChildren;
|
||
|
|
|
||
|
|
/// Specifies the [SpeedDialChild] label text style.
|
||
|
|
final TextStyle? labelsStyle;
|
||
|
|
|
||
|
|
/// The background color of the labels.
|
||
|
|
final Color? labelsBackgroundColor;
|
||
|
|
|
||
|
|
/// An animation controller for the [SpeedDial].
|
||
|
|
///
|
||
|
|
/// Provide an [AnimationController] to control the animations
|
||
|
|
/// from outside the [SpeedDial] widget.
|
||
|
|
final AnimationController? controller;
|
||
|
|
|
||
|
|
/// The color of the [SpeedDial] button foreground when closed.
|
||
|
|
///
|
||
|
|
/// The [SpeedDial] foreground will animate to this color when the user
|
||
|
|
/// closes the speed dial.
|
||
|
|
final Color? closedForegroundColor;
|
||
|
|
|
||
|
|
/// The color of the [SpeedDial] button foreground when opened.
|
||
|
|
///
|
||
|
|
/// The [SpeedDial] foreground will animate to this color when the user
|
||
|
|
/// opens the speed dial.
|
||
|
|
final Color? openForegroundColor;
|
||
|
|
|
||
|
|
/// The color of the [SpeedDial] button background when closed.
|
||
|
|
///
|
||
|
|
/// The [SpeedDial] background will animate to this color when the user
|
||
|
|
/// closes the speed dial.
|
||
|
|
final Color? closedBackgroundColor;
|
||
|
|
|
||
|
|
/// The color of the [SpeedDial] button background when open.
|
||
|
|
///
|
||
|
|
/// The [SpeedDial] background will animate to this color when the user
|
||
|
|
/// opens the speed dial.
|
||
|
|
final Color? openBackgroundColor;
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<StatefulWidget> createState() {
|
||
|
|
return _SpeedDialState();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class _SpeedDialState extends State<SpeedDial>
|
||
|
|
with SingleTickerProviderStateMixin {
|
||
|
|
late AnimationController _animationController;
|
||
|
|
late Animation<Color?> _backgroundColorAnimation;
|
||
|
|
late Animation<Color?> _foregroundColorAnimation;
|
||
|
|
final List<Animation<double>> _speedDialChildAnimations =
|
||
|
|
<Animation<double>>[];
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
_animationController = widget.controller ??
|
||
|
|
AnimationController(
|
||
|
|
vsync: this, duration: const Duration(milliseconds: 100));
|
||
|
|
_animationController.addListener(() {
|
||
|
|
if (mounted) {
|
||
|
|
setState(() {});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
_backgroundColorAnimation = ColorTween(
|
||
|
|
begin: widget.closedBackgroundColor,
|
||
|
|
end: widget.openBackgroundColor,
|
||
|
|
).animate(_animationController);
|
||
|
|
|
||
|
|
_foregroundColorAnimation = ColorTween(
|
||
|
|
begin: widget.closedForegroundColor,
|
||
|
|
end: widget.openForegroundColor,
|
||
|
|
).animate(_animationController);
|
||
|
|
|
||
|
|
final double fractionOfOneSpeedDialChild =
|
||
|
|
1.0 / widget.speedDialChildren.length;
|
||
|
|
for (int speedDialChildIndex = 0;
|
||
|
|
speedDialChildIndex < widget.speedDialChildren.length;
|
||
|
|
++speedDialChildIndex) {
|
||
|
|
final List<TweenSequenceItem<double>> tweenSequenceItems =
|
||
|
|
<TweenSequenceItem<double>>[];
|
||
|
|
|
||
|
|
final double firstWeight =
|
||
|
|
fractionOfOneSpeedDialChild * speedDialChildIndex;
|
||
|
|
if (firstWeight > 0.0) {
|
||
|
|
tweenSequenceItems.add(TweenSequenceItem<double>(
|
||
|
|
tween: ConstantTween<double>(0.0),
|
||
|
|
weight: firstWeight,
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
tweenSequenceItems.add(TweenSequenceItem<double>(
|
||
|
|
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||
|
|
weight: fractionOfOneSpeedDialChild,
|
||
|
|
));
|
||
|
|
|
||
|
|
final double lastWeight = fractionOfOneSpeedDialChild *
|
||
|
|
(widget.speedDialChildren.length - 1 - speedDialChildIndex);
|
||
|
|
if (lastWeight > 0.0) {
|
||
|
|
tweenSequenceItems.add(TweenSequenceItem<double>(
|
||
|
|
tween: ConstantTween<double>(1.0), weight: lastWeight));
|
||
|
|
}
|
||
|
|
|
||
|
|
_speedDialChildAnimations.insert(
|
||
|
|
0,
|
||
|
|
TweenSequence<double>(tweenSequenceItems)
|
||
|
|
.animate(_animationController));
|
||
|
|
}
|
||
|
|
|
||
|
|
super.initState();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_animationController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
//
|
||
|
|
|
||
|
|
int speedDialChildAnimationIndex = 0;
|
||
|
|
|
||
|
|
return _animationController.isDismissed
|
||
|
|
? GestureDetector(
|
||
|
|
onTap: () {
|
||
|
|
if (_animationController.isDismissed) {
|
||
|
|
_animationController.forward();
|
||
|
|
} else {
|
||
|
|
_animationController.reverse();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
child: Container(
|
||
|
|
width: 60,
|
||
|
|
height: 60,
|
||
|
|
clipBehavior: Clip.none,
|
||
|
|
padding: const EdgeInsets.all(14),
|
||
|
|
margin: EdgeInsets.only(
|
||
|
|
top: 18,
|
||
|
|
right: Directionality.of(context) == TextDirection.ltr ? 16 : 0,
|
||
|
|
left: Directionality.of(context) == TextDirection.rtl ? 16 : 0,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
// borderRadius: BorderRadius.circular(60),
|
||
|
|
border: Border.all(color: AppColors.colorPrimary, width: 2),
|
||
|
|
),
|
||
|
|
child: widget.child,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
: GestureDetector(
|
||
|
|
onTap: () {
|
||
|
|
if (_animationController.isDismissed) {
|
||
|
|
_animationController.forward();
|
||
|
|
} else {
|
||
|
|
_animationController.reverse();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
child: OverflowBox(
|
||
|
|
maxWidth: MediaQuery.sizeOf(context).width + 32,
|
||
|
|
child: Container(
|
||
|
|
color: Colors.black.withOpacity(0.4),
|
||
|
|
width: MediaQuery.sizeOf(context).width + 32,
|
||
|
|
child: Column(
|
||
|
|
mainAxisSize: _animationController.isDismissed
|
||
|
|
? MainAxisSize.max
|
||
|
|
: MainAxisSize.max,
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
||
|
|
children: <Widget>[
|
||
|
|
if (!_animationController.isDismissed)
|
||
|
|
Padding(
|
||
|
|
padding: EdgeInsets.only(
|
||
|
|
right: Directionality.of(context) == TextDirection.ltr
|
||
|
|
? 24
|
||
|
|
: 0,
|
||
|
|
left: Directionality.of(context) == TextDirection.rtl
|
||
|
|
? 24
|
||
|
|
: 0,
|
||
|
|
),
|
||
|
|
child: GestureDetector(
|
||
|
|
onTap: () {
|
||
|
|
if (_animationController.isDismissed) {
|
||
|
|
_animationController.forward();
|
||
|
|
} else {
|
||
|
|
_animationController.reverse();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||
|
|
children: widget.speedDialChildren
|
||
|
|
.map<Widget>((SpeedDialChild speedDialChild) {
|
||
|
|
final Widget speedDialChildWidget = Opacity(
|
||
|
|
opacity: _speedDialChildAnimations[
|
||
|
|
speedDialChildAnimationIndex]
|
||
|
|
.value,
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||
|
|
children: <Widget>[
|
||
|
|
if (speedDialChild.label != null)
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.only(
|
||
|
|
right: 16.0 - 4.0),
|
||
|
|
child: GestureDetector(
|
||
|
|
onTap: () => _onTap(speedDialChild),
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: 16.0,
|
||
|
|
vertical: 8.0),
|
||
|
|
child: Text(
|
||
|
|
speedDialChild.label!,
|
||
|
|
style: widget.labelsStyle,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
ScaleTransition(
|
||
|
|
scale: _speedDialChildAnimations[
|
||
|
|
speedDialChildAnimationIndex],
|
||
|
|
child: GestureDetector(
|
||
|
|
onTap: () => _onTap(speedDialChild),
|
||
|
|
child: Container(
|
||
|
|
width: 45,
|
||
|
|
height: 45,
|
||
|
|
clipBehavior: Clip.none,
|
||
|
|
padding: const EdgeInsets.all(10),
|
||
|
|
margin: EdgeInsets.only(
|
||
|
|
top: 18,
|
||
|
|
right: Directionality.of(context) ==
|
||
|
|
TextDirection.ltr
|
||
|
|
? 14
|
||
|
|
: 0,
|
||
|
|
left: Directionality.of(context) ==
|
||
|
|
TextDirection.rtl
|
||
|
|
? 14
|
||
|
|
: 0,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
// borderRadius: BorderRadius.circular(60),
|
||
|
|
border: Border.all(
|
||
|
|
color: AppColors.colorPrimary,
|
||
|
|
width: 2),
|
||
|
|
),
|
||
|
|
child: speedDialChild.child,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
speedDialChildAnimationIndex++;
|
||
|
|
return speedDialChildWidget;
|
||
|
|
}).toList(),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
GestureDetector(
|
||
|
|
onTap: () {
|
||
|
|
if (_animationController.isDismissed) {
|
||
|
|
_animationController.forward();
|
||
|
|
} else {
|
||
|
|
_animationController.reverse();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
child: Container(
|
||
|
|
width: 60,
|
||
|
|
height: 60,
|
||
|
|
clipBehavior: Clip.none,
|
||
|
|
padding: const EdgeInsets.all(14),
|
||
|
|
margin: EdgeInsets.only(
|
||
|
|
top: 18,
|
||
|
|
right: Directionality.of(context) == TextDirection.ltr
|
||
|
|
? 32
|
||
|
|
: 0,
|
||
|
|
left: Directionality.of(context) == TextDirection.rtl
|
||
|
|
? 32
|
||
|
|
: 0,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
// borderRadius: BorderRadius.circular(60),
|
||
|
|
border: Border.all(
|
||
|
|
color: AppColors.colorPrimary, width: 2),
|
||
|
|
),
|
||
|
|
child: widget.child,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
SizedBox(height: 24),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _onTap(SpeedDialChild speedDialChild) {
|
||
|
|
if (speedDialChild.closeSpeedDialOnPressed) {
|
||
|
|
_animationController.reverse();
|
||
|
|
}
|
||
|
|
speedDialChild.onPressed.call();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Padding(
|
||
|
|
// padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||
|
|
// child: FloatingActionButton(
|
||
|
|
// heroTag: speedDialChildAnimationIndex,
|
||
|
|
// mini: true,
|
||
|
|
// foregroundColor: speedDialChild.foregroundColor,
|
||
|
|
// backgroundColor: speedDialChild.backgroundColor,
|
||
|
|
// onPressed: () => _onTap(speedDialChild),
|
||
|
|
// child: speedDialChild.child,
|
||
|
|
// ),
|
||
|
|
// )
|
||
|
|
}
|
||
|
|
|
||
|
|
class SpeedDialChild {
|
||
|
|
const SpeedDialChild({
|
||
|
|
required this.child,
|
||
|
|
required this.onPressed,
|
||
|
|
this.foregroundColor,
|
||
|
|
this.backgroundColor,
|
||
|
|
this.label,
|
||
|
|
this.closeSpeedDialOnPressed = true,
|
||
|
|
});
|
||
|
|
|
||
|
|
/// A widget to display as the [SpeedDialChild].
|
||
|
|
final Widget child;
|
||
|
|
|
||
|
|
/// A callback to be executed when the [SpeedDialChild] is pressed.
|
||
|
|
final Function onPressed;
|
||
|
|
|
||
|
|
/// The [SpeedDialChild] foreground color.
|
||
|
|
final Color? foregroundColor;
|
||
|
|
|
||
|
|
/// The [SpeedDialChild] background color.
|
||
|
|
final Color? backgroundColor;
|
||
|
|
|
||
|
|
/// The text displayed next to the [SpeedDialChild] when the [SpeedDial] is open.
|
||
|
|
final String? label;
|
||
|
|
|
||
|
|
/// Whether the [SpeedDial] should close after the [onPressed] callback of
|
||
|
|
/// [SpeedDialChild] is called.
|
||
|
|
///
|
||
|
|
/// Defaults to [true].
|
||
|
|
final bool closeSpeedDialOnPressed;
|
||
|
|
}
|