Files
jinzhi/lib/common/theme/app_icons.dart
jingyun 3b089e2205 feat: 对齐常驻商城与七鱼客服交互
- 同步商城 H5 登录态与原生 token
- 完善商城/原生 tab 切换、动画及返回交互
- 修复七鱼客服键盘避让和状态栏恢复
- 调整开屏跳过按钮与登录入口交互
2026-09-07 15:47:01 +08:00

200 lines
5.7 KiB
Dart

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<AppBottomTabIcon> createState() => _AppBottomTabIconState();
}
class _AppBottomTabIconState extends State<AppBottomTabIcon>
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<double> _scaleAnimation = TweenSequence<double>([
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,
),
};
}