import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_remix/flutter_remix.dart'; import 'package:not_static_icons/not_static_icons.dart' as nsi; // flutter_remix 0.0.3: icon class is FlutterRemix class AppIcons { static const assetsFill = FlutterRemix.wallet_3_fill; static const assetsLine = FlutterRemix.wallet_3_line; static const marketFill = FlutterRemix.line_chart_fill; static const marketLine = FlutterRemix.line_chart_line; static const newsFill = FlutterRemix.article_fill; static const newsLine = FlutterRemix.article_line; static const meFill = FlutterRemix.user_3_fill; static const meLine = FlutterRemix.user_3_line; static const arrowRight = FlutterRemix.arrow_right_s_line; static const arrowLeft = FlutterRemix.arrow_left_s_line; } enum AppBottomTabSymbol { assets, market, news, profile } class AppBottomTabIcon extends StatefulWidget { const AppBottomTabIcon({ required this.symbol, required this.selected, required this.unselectedColor, required this.selectedColor, this.size = 24, super.key, }); final AppBottomTabSymbol symbol; final bool selected; final Color unselectedColor; final Color selectedColor; final double size; @override State createState() => _AppBottomTabIconState(); } class _AppBottomTabIconState extends State with TickerProviderStateMixin { static const _iconMotionDuration = Duration(milliseconds: 420); static const _selectionDuration = Duration(milliseconds: 140); static const _popDuration = Duration(milliseconds: 260); static const _scalePeak = 1.06; final nsi.AnimatedIconController _iconController = nsi.AnimatedIconController(); late final AnimationController _selectionController = AnimationController( vsync: this, duration: _selectionDuration, reverseDuration: _selectionDuration, value: widget.selected ? 1 : 0, ); late final AnimationController _popController = AnimationController( vsync: this, duration: _popDuration, ); late final CurvedAnimation _colorProgress = CurvedAnimation( parent: _selectionController, curve: Curves.easeOutCubic, reverseCurve: Curves.easeOutCubic, ); late final Animation _scaleAnimation = TweenSequence([ TweenSequenceItem( tween: Tween( begin: 1.0, end: _scalePeak, ).chain(CurveTween(curve: Curves.easeOutBack)), weight: 55, ), TweenSequenceItem( tween: Tween( begin: _scalePeak, end: 1.0, ).chain(CurveTween(curve: Curves.easeOutCubic)), weight: 45, ), ]).animate(_popController); bool get _reduceMotion => MediaQuery.disableAnimationsOf(context); @override void initState() { super.initState(); if (widget.selected) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_reduceMotion) _iconController.animate(); }); } } @override void didUpdateWidget(covariant AppBottomTabIcon oldWidget) { super.didUpdateWidget(oldWidget); if (widget.selected == oldWidget.selected) return; if (widget.selected) { if (_reduceMotion) { _selectionController.value = 1; _popController.value = 0; } else { unawaited(_selectionController.forward(from: 0)); unawaited(_popController.forward(from: 0)); _iconController.animate(); } } else { if (_reduceMotion) { _selectionController.value = 0; } else { unawaited(_selectionController.reverse()); } _popController.value = 0; _iconController.reset(); } } @override void dispose() { _colorProgress.dispose(); _selectionController.dispose(); _popController.dispose(); _iconController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final reduceMotion = _reduceMotion; return SizedBox.square( dimension: widget.size, child: AnimatedBuilder( animation: Listenable.merge([_selectionController, _popController]), builder: (context, _) { final color = Color.lerp( widget.unselectedColor, widget.selectedColor, reduceMotion ? (widget.selected ? 1 : 0) : _colorProgress.value, )!; return Transform.scale( scale: reduceMotion ? 1 : _scaleAnimation.value, child: _buildBottomTabIcon( widget.symbol, size: widget.size, color: color, controller: _iconController, ), ); }, ), ); } } Widget _buildBottomTabIcon( AppBottomTabSymbol symbol, { required double size, required Color color, required nsi.AnimatedIconController controller, }) { const strokeWidth = 2.05; const duration = _AppBottomTabIconState._iconMotionDuration; return switch (symbol) { AppBottomTabSymbol.assets => nsi.CoinsIcon( size: size, color: color, animationDuration: duration, strokeWidth: strokeWidth, interactive: false, controller: controller, ), AppBottomTabSymbol.market => nsi.ChartCandlestickIcon( size: size, color: color, animationDuration: duration, strokeWidth: strokeWidth, interactive: false, controller: controller, ), AppBottomTabSymbol.news => nsi.BookOpenTextIcon( size: size, color: color, animationDuration: duration, strokeWidth: strokeWidth, interactive: false, controller: controller, ), AppBottomTabSymbol.profile => nsi.CircleUserIcon( size: size, color: color, animationDuration: duration, strokeWidth: strokeWidth, interactive: false, controller: controller, ), }; }