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.
		
		
		
		
		
			
		
			
				
	
	
		
			468 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			468 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({
 | 
						|
    Key? key,
 | 
						|
    required this.text,
 | 
						|
    required this.onPressed,
 | 
						|
    required this.controller,
 | 
						|
    this.textColor,
 | 
						|
    required this.loadingColor,
 | 
						|
    this.color,
 | 
						|
  }) : super(key: key);
 | 
						|
 | 
						|
  final String text;
 | 
						|
  final Color? color;
 | 
						|
  final Color? textColor;
 | 
						|
  final Color loadingColor;
 | 
						|
  final VoidCallback? onPressed;
 | 
						|
  final AnimationController controller;
 | 
						|
 | 
						|
  @override
 | 
						|
  _AnimatedButtonState createState() => _AnimatedButtonState();
 | 
						|
}
 | 
						|
 | 
						|
class _AnimatedButtonState extends State<AnimatedButton>
 | 
						|
    with SingleTickerProviderStateMixin {
 | 
						|
  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;
 | 
						|
 | 
						|
  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;
 | 
						|
 | 
						|
    _color = (widget.color ?? buttonTheme.backgroundColor)!;
 | 
						|
    _loadingColor = widget.loadingColor ?? theme.colorScheme.secondary;
 | 
						|
 | 
						|
    _colorAnimation = ColorTween(
 | 
						|
 | 
						|
      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);
 | 
						|
    // final fontSize = theme.textTheme.button!.fontSize;
 | 
						|
    final fontSize = 12.0;
 | 
						|
    final renderParagraph = RenderParagraph(
 | 
						|
      TextSpan(
 | 
						|
        text: widget.text,
 | 
						|
        style: TextStyle(
 | 
						|
          fontSize: fontSize,
 | 
						|
          fontWeight: FontWeight.normal,
 | 
						|
          letterSpacing: 0.6,
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      textDirection: TextDirection.ltr,
 | 
						|
      maxLines: 1,
 | 
						|
    );
 | 
						|
 | 
						|
    renderParagraph.layout(BoxConstraints(minWidth: 120.0));
 | 
						|
 | 
						|
    // text width based on fontSize, plus 45.0 for padding
 | 
						|
    var textWidth =
 | 
						|
        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,
 | 
						|
            elevation: (_isLoading == false)
 | 
						|
                ? (_hover == true
 | 
						|
                    ? buttonTheme.highlightElevation ?? 0.0
 | 
						|
                    : buttonTheme.elevation ?? 0.0)
 | 
						|
                : 0.0,
 | 
						|
          ),
 | 
						|
          child: InkWell(
 | 
						|
            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(
 | 
						|
              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({
 | 
						|
    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({
 | 
						|
    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();
 | 
						|
 | 
						|
  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((_) {
 | 
						|
      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));
 | 
						|
 | 
						|
  Size? getWidgetSize(GlobalKey key) {
 | 
						|
    return key.currentContext!.size;
 | 
						|
    //return renderBox?.size;
 | 
						|
  }
 | 
						|
}
 |