Files
xianyan/lib/app/layout/adaptive_nav_bar.dart
Developer f7520b17b2 win提交
2026-06-22 03:50:59 +08:00

440 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 自适应导航栏
/// 创建时间: 2026-05-29
/// 更新时间: 2026-06-22
/// 作用: 宽屏时垂直导航栏窄屏时底部导航栏支持4种停靠位置
/// 上次更新: "口"按钮改为弹出 3×2 窗口大小预设网格菜单OverlayEntry 定位弹出)
/// ============================================================
import 'dart:ui';
import 'package:badges/badges.dart' as badges;
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/split_view_provider.dart';
import '../../core/theme/app_theme.dart';
import '../../core/theme/app_spacing.dart';
import '../../core/theme/glass_tokens.dart';
import '../../core/utils/platform/platform_utils.dart' as pu;
import '../../features/discover/providers/chat_provider.dart';
import '../../features/settings/providers/theme_settings_provider.dart';
import '../../shared/widgets/animation/tab_icon_sprite.dart';
import '../../shared/widgets/window_size_popup.dart';
class AdaptiveNavBar extends ConsumerWidget {
const AdaptiveNavBar({
required this.currentIndex,
required this.onTabSelected,
super.key,
});
final int currentIndex;
final ValueChanged<int> onTabSelected;
@override
Widget build(BuildContext context, WidgetRef ref) {
final splitState = ref.watch(splitViewProvider);
final position = splitState.navBarPosition;
return switch (position) {
NavBarPosition.left ||
NavBarPosition.right => _buildVertical(context, ref),
NavBarPosition.top ||
NavBarPosition.bottom => _buildHorizontal(context, ref),
};
}
Widget _buildVertical(BuildContext context, WidgetRef ref) {
final ext = AppTheme.ext(context);
final settings = ref.watch(themeSettingsProvider);
final unreadCount = ref.watch(chatProvider).unreadCount;
final animIntensity = settings.animationIntensity.durationMultiplier;
final expressionStyle = settings.tabExpressionStyle;
final characterId = settings.tabCharacterStyleId;
final splitState = ref.watch(splitViewProvider);
final isCollapsed = splitState.navBarCollapsed;
final isRight = splitState.navBarPosition == NavBarPosition.right;
Widget buildTab(TabSpriteType type, int index, String label) {
final isSelected = index == currentIndex;
final showBadge = index == 1 && unreadCount > 0;
return GestureDetector(
onTap: () => onTabSelected(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedScale(
scale: isSelected ? 1.08 : 1.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: showBadge
? badges.Badge(
badgeContent: Text(
'',
style: TextStyle(
color: ext.textOnAccent,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
badgeStyle: const badges.BadgeStyle(
badgeColor: CupertinoColors.systemRed,
padding: EdgeInsets.all(3),
),
position: badges.BadgePosition.topEnd(top: -4, end: -6),
child: TabIconSprite(
type: type,
label: '',
isSelected: isSelected,
adjacentDirection: 0,
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
),
)
: TabIconSprite(
type: type,
label: '',
isSelected: isSelected,
adjacentDirection: 0,
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
),
),
// 折叠态隐藏文字标签
AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: isCollapsed
? const SizedBox.shrink()
: Column(
children: [
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 10,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? (ext.isDark ? ext.textInverse : ext.accent)
: (ext.isDark
? ext.textInverse.withValues(
alpha: 0.38,
)
: const Color(0xFFAEAEB2)),
),
),
],
),
),
],
),
),
);
}
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
width: isCollapsed ? 48 : 72,
decoration: BoxDecoration(
color: ext.glassColor.withValues(
alpha: ext.isDark
? GlassTokens.elevatedOpacityDark
: GlassTokens.elevatedOpacityLight,
),
),
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
sigmaY: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
),
child: SafeArea(
// 垂直导航栏:仅处理顶部和侧边安全区域
// 底部不处理Column 内有 Spacer 自适应,避免底部 SafeArea 挤压内容)
bottom: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
buildTab(TabSpriteType.home, 0, '闲言'),
const SizedBox(height: AppSpacing.lg),
buildTab(TabSpriteType.discover, 1, '发现'),
const SizedBox(height: AppSpacing.lg),
buildTab(TabSpriteType.profile, 2, '我的'),
const Spacer(),
// 折叠/展开按钮
_buildCollapseButton(ref, isCollapsed, ext),
],
),
),
),
),
);
}
/// 折叠/展开导航栏按钮
Widget _buildCollapseButton(
WidgetRef ref,
bool isCollapsed,
AppThemeExtension ext,
) {
return GestureDetector(
onTap: () => ref.read(splitViewProvider.notifier).toggleNavBarCollapsed(),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Icon(
isCollapsed
? CupertinoIcons.chevron_right
: CupertinoIcons.chevron_left,
size: 16,
color: ext.textSecondary,
),
),
);
}
Widget _buildHorizontal(BuildContext context, WidgetRef ref) {
final ext = AppTheme.ext(context);
final settings = ref.watch(themeSettingsProvider);
final unreadCount = ref.watch(chatProvider).unreadCount;
final animIntensity = settings.animationIntensity.durationMultiplier;
final expressionStyle = settings.tabExpressionStyle;
final characterId = settings.tabCharacterStyleId;
final splitState = ref.watch(splitViewProvider);
final isTop = splitState.navBarPosition == NavBarPosition.top;
Widget buildTab(TabSpriteType type, int index, String label) {
final isSelected = index == currentIndex;
final showBadge = index == 1 && unreadCount > 0;
return Expanded(
child: GestureDetector(
onTap: () => onTabSelected(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedScale(
scale: isSelected ? 1.08 : 1.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: showBadge
? badges.Badge(
badgeContent: Text(
'',
style: TextStyle(
color: ext.textOnAccent,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
badgeStyle: const badges.BadgeStyle(
badgeColor: CupertinoColors.systemRed,
padding: EdgeInsets.all(3),
),
position: badges.BadgePosition.topEnd(
top: -4,
end: -6,
),
child: TabIconSprite(
type: type,
label: '',
isSelected: isSelected,
adjacentDirection: 0,
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
),
)
: TabIconSprite(
type: type,
label: '',
isSelected: isSelected,
adjacentDirection: 0,
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
),
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? (ext.isDark ? ext.textInverse : ext.accent)
: (ext.isDark
? ext.textInverse.withValues(alpha: 0.38)
: const Color(0xFFAEAEB2)),
),
),
],
),
),
),
);
}
return Container(
height: 52,
decoration: BoxDecoration(
color: ext.glassColor.withValues(
alpha: ext.isDark
? GlassTokens.elevatedOpacityDark
: GlassTokens.elevatedOpacityLight,
),
),
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
sigmaY: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
buildTab(TabSpriteType.home, 0, '闲言'),
buildTab(TabSpriteType.discover, 1, '发现'),
buildTab(TabSpriteType.profile, 2, '我的'),
if (isTop && pu.isDesktop)
_buildDesktopWindowControls(context, ext),
],
),
),
),
);
}
/// 桌面端窗口控制按钮(最小化/最大化/关闭)+ 可拖拽标题栏区域
Widget _buildDesktopWindowControls(
BuildContext context,
AppThemeExtension ext,
) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// Windows: C++ HTTRANSPARENT 方案处理拖拽,无需 Dart 侧 startDragging
if (pu.isWindows)
const SizedBox(height: 52, width: 40)
else
GestureDetector(
onPanDown: (_) => windowManager.startDragging(),
onDoubleTap: () async {
if (await windowManager.isMaximized()) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
},
child: const SizedBox(height: 52, width: 40),
),
_WindowControlBtn(
icon: CupertinoIcons.minus,
ext: ext,
onPressed: (_) => windowManager.minimize(),
),
_WindowControlBtn(
icon: CupertinoIcons.square,
ext: ext,
iconSize: 12,
onPressed: (btnCtx) => _showWindowSizeMenu(btnCtx),
),
_WindowControlBtn(
icon: CupertinoIcons.xmark,
ext: ext,
onPressed: (_) => windowManager.close(),
isClose: true,
),
],
);
}
/// 窗口大小预设菜单
/// 点击"口"按钮在按钮下方弹出 3×2 网格菜单,选择窗口预设尺寸
void _showWindowSizeMenu(BuildContext buttonContext) {
WindowSizePopup.show(buttonContext);
}
}
/// 窗口控制按钮(最小化/最大化/关闭)
class _WindowControlBtn extends StatefulWidget {
const _WindowControlBtn({
required this.icon,
required this.ext,
required this.onPressed,
this.iconSize = 14,
this.isClose = false,
});
final IconData icon;
final AppThemeExtension ext;
/// 点击回调,传入按钮自身的 BuildContext 用于弹出菜单定位
final void Function(BuildContext buttonContext) onPressed;
final double iconSize;
final bool isClose;
@override
State<_WindowControlBtn> createState() => _WindowControlBtnState();
}
class _WindowControlBtnState extends State<_WindowControlBtn> {
bool _isHovering = false;
@override
Widget build(BuildContext context) {
final ext = widget.ext;
final bgColor = _isHovering
? (widget.isClose
? CupertinoColors.systemRed.withValues(alpha: 0.9)
: ext.textHint.withValues(alpha: 0.15))
: const Color(0x00000000);
return MouseRegion(
onEnter: (_) => setState(() => _isHovering = true),
onExit: (_) => setState(() => _isHovering = false),
child: GestureDetector(
onTap: () => widget.onPressed(context),
child: Container(
width: 46,
height: 52,
color: bgColor,
child: Icon(
widget.icon,
size: widget.iconSize,
color: _isHovering && widget.isClose
? CupertinoColors.white
: ext.textSecondary,
),
),
),
);
}
}