Files
jinzhi/lib/common/router/tab_shell.dart
T

226 lines
6.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../theme/app_icons.dart';
class TabShell extends StatefulWidget {
const TabShell({super.key, required this.child, required this.location});
final Widget child;
final String location;
@override
State<TabShell> createState() => _TabShellState();
}
class _TabShellState extends State<TabShell> {
static const double _compactHeaderThreshold = 34;
bool _showCompactHeader = false;
static const _tabs = ['/assets', '/market', '/news', '/profile'];
int get _index =>
_tabs.indexWhere((t) => widget.location.startsWith(t)).clamp(0, 3);
@override
void didUpdateWidget(covariant TabShell oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.location != widget.location && _showCompactHeader) {
_showCompactHeader = false;
}
}
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final header = _headerForPath(widget.location);
final canPop = GoRouter.of(context).canPop();
return Scaffold(
backgroundColor: cs.background,
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: cs.background,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
toolbarHeight: 32,
leading: canPop
? IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.chevron_left, size: 20),
)
: null,
title: AnimatedOpacity(
opacity: _showCompactHeader ? 1 : 0,
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
header.title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: cs.foreground,
height: 1.1,
letterSpacing: 0,
),
),
if (header.subtitle != null) ...[
const SizedBox(height: 2),
Text(
header.subtitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: cs.mutedForeground,
height: 1.1,
letterSpacing: 0,
),
),
],
],
),
),
),
body: ColoredBox(
color: cs.background,
child: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: Stack(children: [Positioned.fill(child: widget.child)]),
),
),
bottomNavigationBar: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: cs.border, width: 1)),
),
child: SafeArea(
top: false,
child: SizedBox(
height: 62,
child: Row(
children: [
for (var i = 0; i < _tabs.length; i++)
Expanded(
child: _TabButton(
icon: i == _index ? _filledIcons[i] : _outlineIcons[i],
label: _labels[i],
selected: i == _index,
onTap: () => context.go(_tabs[i]),
),
),
],
),
),
),
),
);
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.metrics.axis != Axis.vertical) {
return false;
}
final next = notification.metrics.pixels > _compactHeaderThreshold;
if (next != _showCompactHeader && mounted) {
setState(() => _showCompactHeader = next);
}
return false;
}
}
class _TabButton extends StatelessWidget {
const _TabButton({
required this.icon,
required this.label,
required this.selected,
required this.onTap,
});
final IconData icon;
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final color = selected ? cs.primary : cs.mutedForeground;
return InkWell(
onTap: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// if (selected)
// Container(
// width: 22,
// height: 2.5,
// margin: const EdgeInsets.only(bottom: 6),
// decoration: BoxDecoration(
// color: cs.primary,
// borderRadius: const BorderRadius.vertical(
// bottom: Radius.circular(3),
// ),
// ),
// )
// else
// const SizedBox(height: 8.5),
Icon(icon, size: 22, color: color),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: color,
letterSpacing: 0,
),
),
],
),
);
}
}
const _filledIcons = [
AppIcons.assetsFill,
AppIcons.marketFill,
AppIcons.newsFill,
AppIcons.meFill,
];
const _outlineIcons = [
AppIcons.assetsLine,
AppIcons.marketLine,
AppIcons.newsLine,
AppIcons.meLine,
];
const _labels = ['资产', '行情', '资讯', '我的'];
_ShellHeader _headerForPath(String path) {
return switch (path) {
'/market' => const _ShellHeader('行情', ''),
'/news' => const _ShellHeader('资讯', ''),
'/profile' => const _ShellHeader('我的'),
_ => const _ShellHeader('金值', ''),
};
}
class _ShellHeader {
const _ShellHeader(this.title, [this.subtitle]);
final String title;
final String? subtitle;
}