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.
PatientApp-KKUMC/lib/widgets/LoadingButton.dart

467 lines
12 KiB
Dart

import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class AnimatedButton extends StatefulWidget {
AnimatedButton({
1 year ago
Key? key,
required this.text,
required this.onPressed,
required this.controller,
this.textColor,
1 year ago
required this.loadingColor,
this.color,
}) : super(key: key);
final String text;
1 year ago
final Color? color;
final Color? textColor;
final Color loadingColor;
1 year ago
final VoidCallback? onPressed;
final AnimationController controller;
@override
_AnimatedButtonState createState() => _AnimatedButtonState();
}
class _AnimatedButtonState extends State<AnimatedButton>
with SingleTickerProviderStateMixin {
1 year ago
late Animation<double> _sizeAnimation;
late Animation<double> _textOpacityAnimation;
late Animation<double> _buttonOpacityAnimation;
late Animation<double> _ringThicknessAnimation;
late Animation<double> _ringOpacityAnimation;
late Animation<Color?> _colorAnimation;
var _isLoading = false;
var _hover = false;
var _width = 120.0;
1 year ago
late Color _color;
late Color _loadingColor;
static const _height = 40.0;
static const _loadingCircleRadius = _height / 2;
static const _loadingCircleThickness = 4.0;
@override
void initState() {
super.initState();
_textOpacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: widget.controller,
curve: Interval(0.0, .25),
),
);
// _colorAnimation
// _width, _sizeAnimation
_buttonOpacityAnimation =
Tween<double>(begin: 1.0, end: 0.0).animate(CurvedAnimation(
parent: widget.controller,
curve: Threshold(.65),
));
_ringThicknessAnimation =
Tween<double>(begin: _loadingCircleRadius, end: _loadingCircleThickness)
.animate(CurvedAnimation(
parent: widget.controller,
curve: Interval(.65, .85),
));
_ringOpacityAnimation =
Tween<double>(begin: 1.0, end: 0.0).animate(CurvedAnimation(
parent: widget.controller,
curve: Interval(.85, 1.0),
));
widget.controller.addStatusListener(handleStatusChanged);
}
@override
void didChangeDependencies() {
_updateColorAnimation();
_updateWidth();
super.didChangeDependencies();
}
void _updateColorAnimation() {
final theme = Theme.of(context);
final buttonTheme = theme.floatingActionButtonTheme;
1 year ago
_color = (widget.color ?? buttonTheme.backgroundColor)!;
_loadingColor = widget.loadingColor ?? theme.colorScheme.secondary;
_colorAnimation = ColorTween(
1 year ago
begin: _color,
end: _loadingColor,
).animate(
CurvedAnimation(
parent: widget.controller,
curve: const Interval(0.0, .65, curve: Curves.fastOutSlowIn),
),
);
}
@override
void didUpdateWidget(AnimatedButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.color != widget.color ||
oldWidget.loadingColor != widget.loadingColor) {
_updateColorAnimation();
}
if (oldWidget.text != widget.text) {
_updateWidth();
}
}
@override
void dispose() {
super.dispose();
widget.controller.removeStatusListener(handleStatusChanged);
}
void handleStatusChanged(status) {
if (status == AnimationStatus.forward) {
setState(() => _isLoading = true);
}
if (status == AnimationStatus.dismissed) {
setState(() => _isLoading = false);
}
}
/// sets width and size animation
void _updateWidth() {
final theme = Theme.of(context);
1 year ago
final fontSize = theme.textTheme.button!.fontSize;
final renderParagraph = RenderParagraph(
TextSpan(
text: widget.text,
style: TextStyle(
fontSize: fontSize,
1 year ago
fontWeight: theme.textTheme.button!.fontWeight,
letterSpacing: theme.textTheme.button!.letterSpacing,
),
),
textDirection: TextDirection.ltr,
maxLines: 1,
);
renderParagraph.layout(BoxConstraints(minWidth: 120.0));
// text width based on fontSize, plus 45.0 for padding
var textWidth =
1 year ago
renderParagraph.getMinIntrinsicWidth(fontSize!).ceilToDouble() + 45.0;
// button width is min 120.0 and max 240.0
_width = textWidth > 120.0 && textWidth < 240.0
? textWidth
: textWidth >= 240.0
? 240.0
: 120.0;
_sizeAnimation = Tween<double>(begin: 1.0, end: _height / _width)
.animate(CurvedAnimation(
parent: widget.controller,
curve: Interval(0.0, .65, curve: Curves.fastOutSlowIn),
));
}
Widget _buildButtonText(ThemeData theme) {
return FadeTransition(
opacity: _textOpacityAnimation,
child: AnimatedText(
text: widget.text,
style: TextStyle(color: widget.textColor ?? Colors.white),
),
);
}
Widget _buildButton(ThemeData theme) {
final buttonTheme = theme.floatingActionButtonTheme;
return FadeTransition(
opacity: _buttonOpacityAnimation,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
child: AnimatedBuilder(
animation: _colorAnimation,
builder: (context, child) => Material(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_height / 2)),
color: _colorAnimation.value,
child: child,
shadowColor: _color,
1 year ago
elevation: (_isLoading == false)
? (_hover == true
? buttonTheme.highlightElevation ?? 0.0
: buttonTheme.elevation ?? 0.0)
: 0.0,
),
child: InkWell(
1 year ago
onTap: !_isLoading ? widget!.onPressed : null,
splashColor: buttonTheme.splashColor,
customBorder: buttonTheme.shape,
onHighlightChanged: (value) => setState(() => _hover = value),
child: SizeTransition(
sizeFactor: _sizeAnimation,
axis: Axis.horizontal,
child: Container(
width: _width,
height: _height,
alignment: Alignment.center,
child: _buildButtonText(theme),
),
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Stack(
alignment: Alignment.center,
children: <Widget>[
FadeTransition(
opacity: _ringOpacityAnimation,
child: AnimatedBuilder(
animation: _ringThicknessAnimation,
builder: (context, child) => Ring(
1 year ago
color: widget!.loadingColor,
size: _height,
thickness: _ringThicknessAnimation.value,
),
),
),
if (_isLoading)
SizedBox(
width: _height - _loadingCircleThickness,
height: _height - _loadingCircleThickness,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(widget.loadingColor),
// backgroundColor: Colors.red,
strokeWidth: _loadingCircleThickness,
),
),
_buildButton(theme),
],
);
}
}
class Ring extends StatelessWidget {
Ring({
1 year ago
Key? key,
required this.color,
this.size = 40.0,
this.thickness = 2.0,
this.value = 1.0,
}) : assert(size - thickness > 0),
assert(thickness >= 0),
super(key: key);
final Color color;
final double size;
final double thickness;
final double value;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size - thickness,
height: size - thickness,
child: thickness == 0
? null
: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(color),
strokeWidth: thickness,
value: value,
),
);
}
}
enum AnimatedTextRotation { up, down }
/// https://medium.com/flutter-community/flutter-challenge-3d-bottom-navigation-bar-48952a5fd996
class AnimatedText extends StatefulWidget {
AnimatedText({
1 year ago
Key? key,
required this.text,
required this.style,
this.textRotation = AnimatedTextRotation.up,
}) : super(key: key);
final String text;
final TextStyle style;
final AnimatedTextRotation textRotation;
@override
_AnimatedTextState createState() => _AnimatedTextState();
}
class _AnimatedTextState extends State<AnimatedText>
with SingleTickerProviderStateMixin {
var _newText = '';
var _oldText = '';
var _layoutHeight = 0.0;
final _textKey = GlobalKey();
1 year ago
late Animation<double> _animation;
late AnimationController _controller;
double get radius => _layoutHeight / 2;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 0.0, end: pi / 2).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
));
_oldText = widget.text;
WidgetsBinding.instance.addPostFrameCallback((_) {
1 year ago
setState(() => _layoutHeight = getWidgetSize(_textKey)!.height);
});
}
@override
void didUpdateWidget(AnimatedText oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.text != oldWidget.text) {
_oldText = oldWidget.text;
_newText = widget.text;
_controller.forward().then((_) {
setState(() {
final t = _oldText;
_oldText = _newText;
_newText = t;
});
_controller.reset();
});
}
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
Matrix4 get _matrix {
// Fix: The text is not centered after applying perspective effect in the web build. Idk why
if (kIsWeb) {
return Matrix4.identity();
}
return Matrix4.identity()..setEntry(3, 2, .006);
}
Matrix4 _getFrontSideUp(double value) {
return _matrix
..translate(
0.0,
-radius * sin(_animation.value),
-radius * cos(_animation.value),
)
..rotateX(-_animation.value); // 0 -> -pi/2
}
Matrix4 _getBackSideUp(double value) {
return _matrix
..translate(
0.0,
radius * cos(_animation.value),
-radius * sin(_animation.value),
)
..rotateX((pi / 2) - _animation.value); // pi/2 -> 0
}
Matrix4 _getFrontSideDown(double value) {
return _matrix
..translate(
0.0,
radius * sin(_animation.value),
-radius * cos(_animation.value),
)
..rotateX(_animation.value); // 0 -> pi/2
}
Matrix4 _getBackSideDown(double value) {
return _matrix
..translate(
0.0,
-radius * cos(_animation.value),
-radius * sin(_animation.value),
)
..rotateX(_animation.value - pi / 2); // -pi/2 -> 0
}
@override
Widget build(BuildContext context) {
final rollUp = widget.textRotation == AnimatedTextRotation.up;
final oldText = Text(
_oldText,
key: _textKey,
style: widget.style,
);
final newText = Text(
_newText,
style: widget.style,
);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) => Stack(
alignment: Alignment.center,
children: <Widget>[
if (_animation.value <= toRadian(85))
Transform(
alignment: Alignment.center,
transform: rollUp
? _getFrontSideUp(_animation.value)
: _getFrontSideDown(_animation.value),
child: oldText,
),
if (_animation.value >= toRadian(5))
Transform(
alignment: Alignment.center,
transform: rollUp
? _getBackSideUp(_animation.value)
: _getBackSideDown(_animation.value),
child: newText,
),
],
),
);
}
// Helpers
double toRadian(double degree) => degree * pi / 180;
double lerp(double start, double end, double percent) =>
(start + percent * (end - start));
1 year ago
Size? getWidgetSize(GlobalKey key) {
return key.currentContext!.size;
//return renderBox?.size;
}
}