- 同步商城 H5 登录态与原生 token - 完善商城/原生 tab 切换、动画及返回交互 - 修复七鱼客服键盘避让和状态栏恢复 - 调整开屏跳过按钮与登录入口交互
200 lines
5.7 KiB
Dart
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,
|
|
),
|
|
};
|
|
}
|