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 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 createState() { return _SpeedDialState(); } } class _SpeedDialState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _backgroundColorAnimation; late Animation _foregroundColorAnimation; final List> _speedDialChildAnimations = >[]; @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> tweenSequenceItems = >[]; final double firstWeight = fractionOfOneSpeedDialChild * speedDialChildIndex; if (firstWeight > 0.0) { tweenSequenceItems.add(TweenSequenceItem( tween: ConstantTween(0.0), weight: firstWeight, )); } tweenSequenceItems.add(TweenSequenceItem( tween: Tween(begin: 0.0, end: 1.0), weight: fractionOfOneSpeedDialChild, )); final double lastWeight = fractionOfOneSpeedDialChild * (widget.speedDialChildren.length - 1 - speedDialChildIndex); if (lastWeight > 0.0) { tweenSequenceItems.add(TweenSequenceItem( tween: ConstantTween(1.0), weight: lastWeight)); } _speedDialChildAnimations.insert( 0, TweenSequence(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: [ 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((SpeedDialChild speedDialChild) { final Widget speedDialChildWidget = Opacity( opacity: _speedDialChildAnimations[ speedDialChildAnimationIndex] .value, child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ 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; }