Last active
July 14, 2022 23:51
-
-
Save contactjavas/7a5db7e083ecba0362e08603d62c5aec to your computer and use it in GitHub Desktop.
easy_loading_button demo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
/// begin_library_part | |
/// | |
/// When writing this example, DartPad didn't support (many/almost all) custom packages | |
/// If you want to directly check for the related code (not the library), | |
/// then please search (in this DartPad) for this keyword: begin_example_part | |
enum EasyButtonState { | |
idle, | |
loading, | |
} | |
enum EasyButtonType { | |
elevated, | |
outlined, | |
text, | |
} | |
class EasyButton extends StatefulWidget { | |
/// Content inside the button when the button state is idle. | |
final Widget idleStateWidget; | |
/// Content inside the button when the button state is loading. | |
final Widget loadingStateWidget; | |
final EasyButtonType type; | |
/// Whether or not to animate the width of the button. | |
/// | |
/// If this is set to `false`, you might want to also check the `useEqualLoadingStateWidgetDimension` parameter and set it to `true`. | |
final bool useWidthAnimation; | |
/// Whether or not to force the `loadingStateWidget` to have equal dimension when `useWidthAnimation` is set to false. | |
/// | |
/// This is useful when you are using `CircularProgressIndicator` as the `loadingStateWidget`. | |
/// | |
/// This parameter will be ignored when `useWidthAnimation` value is `true`. | |
final bool useEqualLoadingStateWidgetDimension; | |
final double width; | |
final double height; | |
/// The gap between button and it's content. | |
/// | |
/// This will be ignored when the `type` parameter value is set to `EasyButtonType.text` | |
final double contentGap; | |
final double borderRadius; | |
/// The elevation of the button. | |
/// | |
/// This will only be applied when the `type` parameter value is `EasyButtonType.elevated` | |
final double elevation; | |
/// Color for the button. | |
/// | |
/// For [`EasyButtonType.elevated`]: This will be the background color. | |
/// | |
/// For [`EasyButtonType.outlined`]: This will be the border color. | |
/// | |
/// For [`EasyButtonType.text`]: This will be the text color. | |
final Color buttonColor; | |
final Function? onPressed; | |
const EasyButton({ | |
Key? key, | |
required this.idleStateWidget, | |
required this.loadingStateWidget, | |
this.type = EasyButtonType.elevated, | |
this.useWidthAnimation = true, | |
this.useEqualLoadingStateWidgetDimension = true, | |
this.width = double.infinity, | |
this.height = 40.0, | |
this.contentGap = 0.0, | |
this.borderRadius = 0.0, | |
this.elevation = 0.0, | |
this.buttonColor = Colors.blueAccent, | |
this.onPressed, | |
}) : super(key: key); | |
@override | |
_EasyButtonState createState() => _EasyButtonState(); | |
} | |
class _EasyButtonState extends State<EasyButton> with TickerProviderStateMixin { | |
final GlobalKey _globalKey = GlobalKey(); | |
Animation? _anim; | |
late AnimationController _animController; | |
final Duration _duration = const Duration( | |
milliseconds: 250, | |
); | |
EasyButtonState _state = EasyButtonState.idle; | |
late double _width; | |
late double _height; | |
late double _borderRadius; | |
@override | |
dispose() { | |
_animController.dispose(); | |
super.dispose(); | |
} | |
@override | |
void deactivate() { | |
_reset(); | |
super.deactivate(); | |
} | |
@override | |
void initState() { | |
_reset(); | |
super.initState(); | |
} | |
void _reset() { | |
_state = EasyButtonState.idle; | |
_width = widget.width; | |
_height = widget.height; | |
_borderRadius = widget.borderRadius; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return PhysicalModel( | |
color: Colors.transparent, | |
borderRadius: BorderRadius.circular(_borderRadius), | |
child: SizedBox( | |
key: _globalKey, | |
height: _height, | |
width: _width, | |
child: _buildChild(context), | |
), | |
); | |
} | |
Widget _buildChild(BuildContext context) { | |
var padding = EdgeInsets.all( | |
widget.contentGap, | |
); | |
var buttonColor = widget.buttonColor; | |
var shape = RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(_borderRadius), | |
); | |
final ButtonStyle elevatedButtonStyle = ElevatedButton.styleFrom( | |
padding: padding, | |
primary: buttonColor, | |
elevation: widget.elevation, | |
shape: shape, | |
); | |
final ButtonStyle outlinedButtonStyle = OutlinedButton.styleFrom( | |
padding: padding, | |
shape: shape, | |
side: BorderSide( | |
color: buttonColor, | |
), | |
); | |
final ButtonStyle textButtonStyle = TextButton.styleFrom( | |
padding: padding, | |
); | |
switch (widget.type) { | |
case EasyButtonType.elevated: | |
return ElevatedButton( | |
style: elevatedButtonStyle, | |
child: _buildChildren(context), | |
onPressed: _onButtonPressed(), | |
); | |
case EasyButtonType.outlined: | |
return TextButton( | |
style: outlinedButtonStyle, | |
child: _buildChildren(context), | |
onPressed: _onButtonPressed(), | |
); | |
case EasyButtonType.text: | |
return TextButton( | |
style: textButtonStyle, | |
child: _buildChildren(context), | |
onPressed: _onButtonPressed(), | |
); | |
} | |
} | |
Widget _buildChildren(BuildContext context) { | |
double contentGap = | |
widget.type == EasyButtonType.text ? 0.0 : widget.contentGap; | |
Widget contentWidget; | |
switch (_state) { | |
case EasyButtonState.idle: | |
contentWidget = widget.idleStateWidget; | |
break; | |
case EasyButtonState.loading: | |
if (!widget.useWidthAnimation && | |
widget.useEqualLoadingStateWidgetDimension) { | |
contentWidget = SizedBox.square( | |
dimension: widget.height - (contentGap * 2), | |
child: widget.loadingStateWidget, | |
); | |
} else { | |
contentWidget = widget.loadingStateWidget; | |
} | |
break; | |
} | |
return contentWidget; | |
} | |
VoidCallback _onButtonPressed() { | |
if (widget.onPressed == null) { | |
return () {}; | |
} | |
return _manageLoadingState; | |
} | |
Future _manageLoadingState() async { | |
if (_state != EasyButtonState.idle) { | |
return; | |
} | |
// The result of widget.onPressed() will be called as VoidCallback after button status is back to default. | |
dynamic onIdle; | |
if (widget.useWidthAnimation) { | |
_toProcessing(); | |
_forward((status) { | |
if (status == AnimationStatus.dismissed) { | |
_toDefault(); | |
if (onIdle != null && | |
(onIdle is VoidCallback || onIdle is FormFieldValidator)) { | |
onIdle(); | |
} | |
} | |
}); | |
onIdle = await widget.onPressed!(); | |
_reverse(); | |
} else { | |
_toProcessing(); | |
onIdle = await widget.onPressed!(); | |
_toDefault(); | |
if (onIdle != null && | |
(onIdle is VoidCallback || onIdle is FormFieldValidator)) { | |
onIdle(); | |
} | |
} | |
} | |
void _toProcessing() { | |
setState(() { | |
_state = EasyButtonState.loading; | |
}); | |
} | |
void _toDefault() { | |
if (mounted) { | |
setState(() { | |
_state = EasyButtonState.idle; | |
}); | |
} else { | |
_state = EasyButtonState.idle; | |
} | |
} | |
void _forward(AnimationStatusListener stateListener) { | |
double initialWidth = _globalKey.currentContext!.size!.width; | |
double initialBorderRadius = widget.borderRadius; | |
double targetWidth = _height; | |
double targetBorderRadius = _height / 2; | |
_animController = AnimationController(duration: _duration, vsync: this); | |
_anim = Tween(begin: 0.0, end: 1.0).animate(_animController) | |
..addListener(() { | |
setState(() { | |
_width = initialWidth - ((initialWidth - targetWidth) * _anim!.value); | |
_borderRadius = initialBorderRadius - | |
((initialBorderRadius - targetBorderRadius) * _anim!.value); | |
}); | |
}) | |
..addStatusListener(stateListener); | |
_animController.forward(); | |
} | |
void _reverse() { | |
_animController.reverse(); | |
} | |
} | |
/// end_library_part | |
/// begin_example_part | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({Key? key}) : super(key: key); | |
// This widget is the root of your application. | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Loading Button', | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: const MyHomePage(title: 'Flutter Loading Button'), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
const MyHomePage({Key? key, required this.title}) : super(key: key); | |
final String title; | |
@override | |
State<MyHomePage> createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
@override | |
Widget build(BuildContext context) { | |
onButtonPressed() async { | |
await Future.delayed(const Duration(milliseconds: 3000), () => 42); | |
// After [onPressed], it will trigger animation running backwards, from end to beginning | |
return () { | |
// Optional returns is returning a VoidCallback that will be called | |
// after the animation is stopped at the beginning. | |
// A best practice would be to do time-consuming task in [onPressed], | |
// and do page navigation in the returned VoidCallback. | |
// So that user won't missed out the reverse animation. | |
}; | |
} | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(widget.title), | |
), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Elevated Button', | |
), | |
const SizedBox( | |
height: 5, | |
), | |
EasyButton( | |
idleStateWidget: const Text( | |
'Elevated Button', | |
style: TextStyle( | |
color: Colors.white, | |
), | |
), | |
loadingStateWidget: const CircularProgressIndicator( | |
strokeWidth: 3.0, | |
valueColor: AlwaysStoppedAnimation<Color>( | |
Colors.white, | |
), | |
), | |
useEqualLoadingStateWidgetDimension: true, | |
useWidthAnimation: false, | |
width: 150.0, | |
height: 40.0, | |
borderRadius: 4.0, | |
elevation: 2.0, | |
contentGap: 6.0, | |
buttonColor: Colors.blueAccent, | |
onPressed: onButtonPressed, | |
), | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Elevated Button - Animated', | |
), | |
const SizedBox( | |
height: 5, | |
), | |
EasyButton( | |
idleStateWidget: const Text( | |
'Elevated Button', | |
style: TextStyle( | |
color: Colors.white, | |
), | |
), | |
loadingStateWidget: const CircularProgressIndicator( | |
strokeWidth: 3.0, | |
valueColor: AlwaysStoppedAnimation<Color>( | |
Colors.white, | |
), | |
), | |
useWidthAnimation: true, | |
width: 150.0, | |
height: 40.0, | |
borderRadius: 4.0, | |
contentGap: 6.0, | |
buttonColor: Colors.blueAccent, | |
onPressed: onButtonPressed, | |
), | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Outlined Button', | |
), | |
const SizedBox( | |
height: 5, | |
), | |
EasyButton( | |
type: EasyButtonType.outlined, | |
idleStateWidget: const Text( | |
'Outlined Button', | |
style: TextStyle( | |
color: Colors.blueAccent, | |
), | |
), | |
loadingStateWidget: const CircularProgressIndicator( | |
strokeWidth: 3.0, | |
valueColor: AlwaysStoppedAnimation<Color>( | |
Colors.blueAccent, | |
), | |
), | |
useEqualLoadingStateWidgetDimension: true, | |
useWidthAnimation: false, | |
width: 150.0, | |
height: 40.0, | |
borderRadius: 4.0, | |
contentGap: 6.0, | |
onPressed: onButtonPressed, | |
), | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Outlined Button - Animated', | |
), | |
const SizedBox( | |
height: 5, | |
), | |
EasyButton( | |
type: EasyButtonType.outlined, | |
idleStateWidget: const Text( | |
'Outlined Button', | |
style: TextStyle( | |
color: Colors.blueAccent, | |
), | |
), | |
loadingStateWidget: const CircularProgressIndicator( | |
strokeWidth: 3.0, | |
valueColor: AlwaysStoppedAnimation<Color>( | |
Colors.blueAccent, | |
), | |
), | |
useWidthAnimation: true, | |
width: 150.0, | |
height: 40.0, | |
borderRadius: 4.0, | |
contentGap: 6.0, | |
onPressed: onButtonPressed, | |
), | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Text Button', | |
), | |
const SizedBox( | |
height: 5, | |
), | |
EasyButton( | |
type: EasyButtonType.text, | |
idleStateWidget: const Text( | |
'Text Button', | |
style: TextStyle( | |
color: Colors.blueAccent, | |
), | |
), | |
loadingStateWidget: const CircularProgressIndicator( | |
strokeWidth: 3.0, | |
valueColor: AlwaysStoppedAnimation<Color>( | |
Colors.blueAccent, | |
), | |
), | |
useEqualLoadingStateWidgetDimension: true, | |
useWidthAnimation: false, | |
width: 150.0, | |
height: 28.0, | |
borderRadius: 4.0, | |
onPressed: onButtonPressed, | |
), | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Elevated Button Fullwidth', | |
), | |
const SizedBox( | |
height: 5, | |
), | |
EasyButton( | |
idleStateWidget: const Text( | |
'Elevated Button Fullwidth', | |
style: TextStyle( | |
color: Colors.white, | |
), | |
), | |
loadingStateWidget: const CircularProgressIndicator( | |
strokeWidth: 3.0, | |
valueColor: AlwaysStoppedAnimation<Color>( | |
Colors.white, | |
), | |
), | |
useEqualLoadingStateWidgetDimension: true, | |
useWidthAnimation: false, | |
width: double.infinity, | |
height: 40.0, | |
contentGap: 6.0, | |
buttonColor: Colors.blueAccent, | |
onPressed: onButtonPressed, | |
), | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Elevated Button Fullwidth - Animated', | |
), | |
const SizedBox( | |
height: 5, | |
), | |
EasyButton( | |
idleStateWidget: const Text( | |
'Elevated Button Fullwidth', | |
style: TextStyle( | |
color: Colors.white, | |
), | |
), | |
loadingStateWidget: const CircularProgressIndicator( | |
strokeWidth: 3.0, | |
valueColor: AlwaysStoppedAnimation<Color>( | |
Colors.white, | |
), | |
), | |
useWidthAnimation: true, | |
width: double.infinity, | |
height: 40.0, | |
contentGap: 6.0, | |
buttonColor: Colors.blueAccent, | |
onPressed: onButtonPressed, | |
), | |
], | |
), | |
), // This trailing comma makes auto-formatting nicer for build methods. | |
); | |
} | |
} | |
/// end_example_part |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment