diff --git a/lib/common/widgets/floating_navigation_bar.dart b/lib/common/widgets/floating_navigation_bar.dart new file mode 100644 index 000000000..267757bd7 --- /dev/null +++ b/lib/common/widgets/floating_navigation_bar.dart @@ -0,0 +1,777 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:PiliPlus/utils/extension/theme_ext.dart'; +import 'package:flutter/material.dart'; + +const double _kMaxLabelTextScaleFactor = 1.3; + +const _kNavigationHeight = 64.0; +const _kIndicatorHeight = _kNavigationHeight - 2 * _kIndicatorPaddingInt; +const _kIndicatorWidth = 86.0; +const _kIndicatorPaddingInt = 4.0; +const _kIndicatorPadding = EdgeInsets.all(_kIndicatorPaddingInt); +const _kBorderRadius = BorderRadius.all(.circular(_kNavigationHeight / 2)); +const _kNavigationShape = RoundedSuperellipseBorder( + borderRadius: _kBorderRadius, +); + +/// ref [NavigationBar] +class FloatingNavigationBar extends StatelessWidget { + // ignore: prefer_const_constructors_in_immutables + FloatingNavigationBar({ + super.key, + this.animationDuration = const Duration(milliseconds: 500), + this.selectedIndex = 0, + required this.destinations, + this.onDestinationSelected, + this.backgroundColor, + this.elevation, + this.shadowColor, + this.surfaceTintColor, + this.indicatorColor, + this.indicatorShape, + this.labelBehavior, + this.overlayColor, + this.labelTextStyle, + this.labelPadding, + this.bottomPadding = 8.0, + }) : assert(destinations.length >= 2), + assert(0 <= selectedIndex && selectedIndex < destinations.length); + + final Duration animationDuration; + final int selectedIndex; + final List destinations; + final ValueChanged? onDestinationSelected; + final Color? backgroundColor; + final double? elevation; + final Color? shadowColor; + final Color? surfaceTintColor; + final Color? indicatorColor; + final ShapeBorder? indicatorShape; + final NavigationDestinationLabelBehavior? labelBehavior; + final WidgetStateProperty? overlayColor; + final WidgetStateProperty? labelTextStyle; + final EdgeInsetsGeometry? labelPadding; + final double bottomPadding; + + VoidCallback _handleTap(int index) { + return onDestinationSelected != null + ? () => onDestinationSelected!(index) + : () {}; + } + + @override + Widget build(BuildContext context) { + final defaults = _NavigationBarDefaultsM3(context); + + final navigationBarTheme = NavigationBarTheme.of(context); + final effectiveLabelBehavior = + labelBehavior ?? + navigationBarTheme.labelBehavior ?? + defaults.labelBehavior!; + + final padding = MediaQuery.viewPaddingOf(context); + + return UnconstrainedBox( + child: Padding( + padding: .fromLTRB( + padding.left, + 0, + padding.right, + bottomPadding + padding.bottom, + ), + child: SizedBox( + height: _kNavigationHeight, + width: destinations.length * _kIndicatorWidth, + child: DecoratedBox( + decoration: ShapeDecoration( + color: ElevationOverlay.applySurfaceTint( + backgroundColor ?? + navigationBarTheme.backgroundColor ?? + defaults.backgroundColor!, + surfaceTintColor ?? + navigationBarTheme.surfaceTintColor ?? + defaults.surfaceTintColor, + elevation ?? + navigationBarTheme.elevation ?? + defaults.elevation!, + ), + shape: RoundedSuperellipseBorder( + side: defaults.borderSide, + borderRadius: _kBorderRadius, + ), + ), + child: Padding( + padding: _kIndicatorPadding, + child: Row( + crossAxisAlignment: .stretch, + children: [ + for (int i = 0; i < destinations.length; i++) + Expanded( + child: _SelectableAnimatedBuilder( + duration: animationDuration, + isSelected: i == selectedIndex, + builder: (context, animation) { + return _NavigationDestinationInfo( + index: i, + selectedIndex: selectedIndex, + totalNumberOfDestinations: destinations.length, + selectedAnimation: animation, + labelBehavior: effectiveLabelBehavior, + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, + overlayColor: overlayColor, + onTap: _handleTap(i), + labelTextStyle: labelTextStyle, + labelPadding: labelPadding, + child: destinations[i], + ); + }, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class FloatingNavigationDestination extends StatelessWidget { + const FloatingNavigationDestination({ + super.key, + required this.icon, + this.selectedIcon, + required this.label, + this.tooltip, + this.enabled = true, + }); + + final Widget icon; + + final Widget? selectedIcon; + + final String label; + + final String? tooltip; + + final bool enabled; + + @override + Widget build(BuildContext context) { + final info = _NavigationDestinationInfo.of(context); + const selectedState = {WidgetState.selected}; + const unselectedState = {}; + const disabledState = {WidgetState.disabled}; + + final navigationBarTheme = NavigationBarTheme.of(context); + final defaults = _NavigationBarDefaultsM3(context); + final animation = info.selectedAnimation; + + return Stack( + alignment: .center, + clipBehavior: .none, + children: [ + NavigationIndicator( + animation: animation, + color: + info.indicatorColor ?? + navigationBarTheme.indicatorColor ?? + defaults.indicatorColor!, + ), + _NavigationDestinationBuilder( + label: label, + tooltip: tooltip, + enabled: enabled, + buildIcon: (context) { + final IconThemeData selectedIconTheme = + navigationBarTheme.iconTheme?.resolve(selectedState) ?? + defaults.iconTheme!.resolve(selectedState)!; + final IconThemeData unselectedIconTheme = + navigationBarTheme.iconTheme?.resolve(unselectedState) ?? + defaults.iconTheme!.resolve(unselectedState)!; + final IconThemeData disabledIconTheme = + navigationBarTheme.iconTheme?.resolve(disabledState) ?? + defaults.iconTheme!.resolve(disabledState)!; + + final Widget selectedIconWidget = IconTheme.merge( + data: enabled ? selectedIconTheme : disabledIconTheme, + child: selectedIcon ?? icon, + ); + final Widget unselectedIconWidget = IconTheme.merge( + data: enabled ? unselectedIconTheme : disabledIconTheme, + child: icon, + ); + return _StatusTransitionWidgetBuilder( + animation: animation, + builder: (context, child) { + return animation.isForwardOrCompleted + ? selectedIconWidget + : unselectedIconWidget; + }, + ); + }, + buildLabel: (context) { + final TextStyle? effectiveSelectedLabelTextStyle = + info.labelTextStyle?.resolve(selectedState) ?? + navigationBarTheme.labelTextStyle?.resolve(selectedState) ?? + defaults.labelTextStyle!.resolve(selectedState); + final TextStyle? effectiveUnselectedLabelTextStyle = + info.labelTextStyle?.resolve(unselectedState) ?? + navigationBarTheme.labelTextStyle?.resolve(unselectedState) ?? + defaults.labelTextStyle!.resolve(unselectedState); + final TextStyle? effectiveDisabledLabelTextStyle = + info.labelTextStyle?.resolve(disabledState) ?? + navigationBarTheme.labelTextStyle?.resolve(disabledState) ?? + defaults.labelTextStyle!.resolve(disabledState); + final EdgeInsetsGeometry labelPadding = + info.labelPadding ?? + navigationBarTheme.labelPadding ?? + defaults.labelPadding!; + + final textStyle = enabled + ? animation.isForwardOrCompleted + ? effectiveSelectedLabelTextStyle + : effectiveUnselectedLabelTextStyle + : effectiveDisabledLabelTextStyle; + + return Padding( + padding: labelPadding, + child: MediaQuery.withClampedTextScaling( + maxScaleFactor: _kMaxLabelTextScaleFactor, + child: Text(label, style: textStyle), + ), + ); + }, + ), + ], + ); + } +} + +class _NavigationDestinationBuilder extends StatefulWidget { + const _NavigationDestinationBuilder({ + required this.buildIcon, + required this.buildLabel, + required this.label, + this.tooltip, + this.enabled = true, + }); + + final WidgetBuilder buildIcon; + + final WidgetBuilder buildLabel; + + final String label; + + final String? tooltip; + + final bool enabled; + + @override + State<_NavigationDestinationBuilder> createState() => + _NavigationDestinationBuilderState(); +} + +class _NavigationDestinationBuilderState + extends State<_NavigationDestinationBuilder> { + final GlobalKey iconKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final info = _NavigationDestinationInfo.of(context); + + final child = GestureDetector( + behavior: .opaque, + onTap: widget.enabled ? info.onTap : null, + child: _NavigationBarDestinationLayout( + icon: widget.buildIcon(context), + iconKey: iconKey, + label: widget.buildLabel(context), + ), + ); + if (info.labelBehavior == .alwaysShow) { + return child; + } + return _NavigationBarDestinationTooltip( + message: widget.tooltip ?? widget.label, + child: child, + ); + } +} + +class _NavigationDestinationInfo extends InheritedWidget { + const _NavigationDestinationInfo({ + required this.index, + required this.selectedIndex, + required this.totalNumberOfDestinations, + required this.selectedAnimation, + required this.labelBehavior, + required this.indicatorColor, + required this.indicatorShape, + required this.overlayColor, + required this.onTap, + this.labelTextStyle, + this.labelPadding, + required super.child, + }); + + final int index; + + final int selectedIndex; + + final int totalNumberOfDestinations; + + final Animation selectedAnimation; + + final NavigationDestinationLabelBehavior labelBehavior; + + final Color? indicatorColor; + + final ShapeBorder? indicatorShape; + + final WidgetStateProperty? overlayColor; + + final VoidCallback onTap; + + final WidgetStateProperty? labelTextStyle; + + final EdgeInsetsGeometry? labelPadding; + + static _NavigationDestinationInfo of(BuildContext context) { + final _NavigationDestinationInfo? result = context + .dependOnInheritedWidgetOfExactType<_NavigationDestinationInfo>(); + assert( + result != null, + 'Navigation destinations need a _NavigationDestinationInfo parent, ' + 'which is usually provided by NavigationBar.', + ); + return result!; + } + + @override + bool updateShouldNotify(_NavigationDestinationInfo oldWidget) { + return index != oldWidget.index || + totalNumberOfDestinations != oldWidget.totalNumberOfDestinations || + selectedAnimation != oldWidget.selectedAnimation || + labelBehavior != oldWidget.labelBehavior || + onTap != oldWidget.onTap; + } +} + +class NavigationIndicator extends StatelessWidget { + const NavigationIndicator({ + super.key, + required this.animation, + this.color, + this.width = _kIndicatorWidth, + this.height = _kIndicatorHeight, + }); + + final Animation animation; + + final Color? color; + + final double width; + + final double height; + + static final _anim = Tween( + begin: .5, + end: 1.0, + ).chain(CurveTween(curve: Curves.easeInOutCubicEmphasized)); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) { + final double scale = animation.isDismissed + ? 0.0 + : _anim.evaluate(animation); + + return Transform( + alignment: Alignment.center, + transform: Matrix4.diagonal3Values(scale, 1.0, 1.0), + child: child, + ); + }, + + child: _StatusTransitionWidgetBuilder( + animation: animation, + builder: (context, child) { + return _SelectableAnimatedBuilder( + isSelected: animation.isForwardOrCompleted, + duration: const Duration(milliseconds: 100), + alwaysDoFullAnimation: true, + builder: (context, fadeAnimation) { + return FadeTransition( + opacity: fadeAnimation, + child: DecoratedBox( + decoration: ShapeDecoration( + shape: _kNavigationShape, + color: color ?? Theme.of(context).colorScheme.secondary, + ), + child: const SizedBox( + width: _kIndicatorWidth, + height: _kIndicatorHeight, + ), + ), + ); + }, + ); + }, + ), + ); + } +} + +class _NavigationBarDestinationLayout extends StatelessWidget { + const _NavigationBarDestinationLayout({ + required this.icon, + required this.iconKey, + required this.label, + }); + + final Widget icon; + + final GlobalKey iconKey; + + final Widget label; + + @override + Widget build(BuildContext context) { + return _DestinationLayoutAnimationBuilder( + builder: (context, animation) { + return CustomMultiChildLayout( + delegate: _NavigationDestinationLayoutDelegate(animation: animation), + children: [ + LayoutId( + id: _NavigationDestinationLayoutDelegate.iconId, + child: KeyedSubtree(key: iconKey, child: icon), + ), + LayoutId( + id: _NavigationDestinationLayoutDelegate.labelId, + child: FadeTransition( + alwaysIncludeSemantics: true, + opacity: animation, + child: label, + ), + ), + ], + ); + }, + ); + } +} + +class _DestinationLayoutAnimationBuilder extends StatelessWidget { + const _DestinationLayoutAnimationBuilder({required this.builder}); + + final Widget Function(BuildContext, Animation) builder; + + @override + Widget build(BuildContext context) { + final info = _NavigationDestinationInfo.of(context); + switch (info.labelBehavior) { + case NavigationDestinationLabelBehavior.alwaysShow: + return builder(context, kAlwaysCompleteAnimation); + case NavigationDestinationLabelBehavior.alwaysHide: + return builder(context, kAlwaysDismissedAnimation); + case NavigationDestinationLabelBehavior.onlyShowSelected: + return _CurvedAnimationBuilder( + animation: info.selectedAnimation, + curve: Curves.easeInOutCubicEmphasized, + reverseCurve: Curves.easeInOutCubicEmphasized.flipped, + builder: builder, + ); + } + } +} + +class _NavigationBarDestinationTooltip extends StatelessWidget { + const _NavigationBarDestinationTooltip({ + required this.message, + required this.child, + }); + + final String message; + + final Widget child; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: message, + verticalOffset: 34, + excludeFromSemantics: true, + preferBelow: false, + child: child, + ); + } +} + +class _NavigationDestinationLayoutDelegate extends MultiChildLayoutDelegate { + _NavigationDestinationLayoutDelegate({required this.animation}) + : super(relayout: animation); + + final Animation animation; + + static const int iconId = 1; + + static const int labelId = 2; + + @override + void performLayout(Size size) { + double halfWidth(Size size) => size.width / 2; + double halfHeight(Size size) => size.height / 2; + + final Size iconSize = layoutChild(iconId, BoxConstraints.loose(size)); + final Size labelSize = layoutChild(labelId, BoxConstraints.loose(size)); + + final double yPositionOffset = Tween( + begin: halfHeight(iconSize), + + end: halfHeight(iconSize) + halfHeight(labelSize), + ).transform(animation.value); + final double iconYPosition = halfHeight(size) - yPositionOffset; + + positionChild( + iconId, + Offset( + halfWidth(size) - halfWidth(iconSize), + iconYPosition, + ), + ); + + positionChild( + labelId, + Offset( + halfWidth(size) - halfWidth(labelSize), + + iconYPosition + iconSize.height, + ), + ); + } + + @override + bool shouldRelayout(_NavigationDestinationLayoutDelegate oldDelegate) { + return oldDelegate.animation != animation; + } +} + +class _StatusTransitionWidgetBuilder extends StatusTransitionWidget { + const _StatusTransitionWidgetBuilder({ + required super.animation, + required this.builder, + // ignore: unused_element_parameter + this.child, + }); + + final TransitionBuilder builder; + + final Widget? child; + + @override + Widget build(BuildContext context) => builder(context, child); +} + +class _SelectableAnimatedBuilder extends StatefulWidget { + const _SelectableAnimatedBuilder({ + required this.isSelected, + this.duration = const Duration(milliseconds: 200), + this.alwaysDoFullAnimation = false, + required this.builder, + }); + + final bool isSelected; + + final Duration duration; + + final bool alwaysDoFullAnimation; + + final Widget Function(BuildContext, Animation) builder; + + @override + _SelectableAnimatedBuilderState createState() => + _SelectableAnimatedBuilderState(); +} + +class _SelectableAnimatedBuilderState extends State<_SelectableAnimatedBuilder> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this); + _controller.duration = widget.duration; + _controller.value = widget.isSelected ? 1.0 : 0.0; + } + + @override + void didUpdateWidget(_SelectableAnimatedBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.duration != widget.duration) { + _controller.duration = widget.duration; + } + if (oldWidget.isSelected != widget.isSelected) { + if (widget.isSelected) { + _controller.forward(from: widget.alwaysDoFullAnimation ? 0 : null); + } else { + _controller.reverse(from: widget.alwaysDoFullAnimation ? 1 : null); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context, _controller); + } +} + +class _CurvedAnimationBuilder extends StatefulWidget { + const _CurvedAnimationBuilder({ + required this.animation, + required this.curve, + required this.reverseCurve, + required this.builder, + }); + + final Animation animation; + final Curve curve; + final Curve reverseCurve; + final Widget Function(BuildContext, Animation) builder; + + @override + _CurvedAnimationBuilderState createState() => _CurvedAnimationBuilderState(); +} + +class _CurvedAnimationBuilderState extends State<_CurvedAnimationBuilder> { + late AnimationStatus _animationDirection; + AnimationStatus? _preservedDirection; + + @override + void initState() { + super.initState(); + _animationDirection = widget.animation.status; + _updateStatus(widget.animation.status); + widget.animation.addStatusListener(_updateStatus); + } + + @override + void dispose() { + widget.animation.removeStatusListener(_updateStatus); + super.dispose(); + } + + void _updateStatus(AnimationStatus status) { + if (_animationDirection != status) { + setState(() { + _animationDirection = status; + }); + } + switch (status) { + case AnimationStatus.forward || AnimationStatus.reverse + when _preservedDirection != null: + break; + case AnimationStatus.forward || AnimationStatus.reverse: + setState(() { + _preservedDirection = status; + }); + case AnimationStatus.completed || AnimationStatus.dismissed: + setState(() { + _preservedDirection = null; + }); + } + } + + @override + Widget build(BuildContext context) { + final shouldUseForwardCurve = + (_preservedDirection ?? _animationDirection) != AnimationStatus.reverse; + + final Animation curvedAnimation = CurveTween( + curve: shouldUseForwardCurve ? widget.curve : widget.reverseCurve, + ).animate(widget.animation); + + return widget.builder(context, curvedAnimation); + } +} + +const _indicatorDark = Color(0x15FFFFFF); +const _indicatorLight = Color(0x10000000); + +class _NavigationBarDefaultsM3 extends NavigationBarThemeData { + _NavigationBarDefaultsM3(this.context) + : super( + height: _kNavigationHeight, + elevation: 3.0, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + ); + + final BuildContext context; + late final _colors = Theme.of(context).colorScheme; + late final _textTheme = Theme.of(context).textTheme; + + BorderSide get borderSide => _colors.brightness.isDark + ? const BorderSide(color: Color(0x08FFFFFF)) + : const BorderSide(color: Color(0x08000000)); + + @override + Color? get backgroundColor => _colors.surfaceContainer; + + @override + Color? get shadowColor => Colors.transparent; + + @override + Color? get surfaceTintColor => Colors.transparent; + + @override + WidgetStateProperty? get iconTheme { + return WidgetStateProperty.resolveWith((Set states) { + return IconThemeData( + size: 24.0, + color: states.contains(WidgetState.disabled) + ? _colors.onSurfaceVariant.withValues(alpha: 0.38) + : states.contains(WidgetState.selected) + ? _colors.onSecondaryContainer + : _colors.onSurfaceVariant, + ); + }); + } + + @override + Color? get indicatorColor => + _colors.brightness.isDark ? _indicatorDark : _indicatorLight; + + @override + ShapeBorder? get indicatorShape => const StadiumBorder(); + + @override + WidgetStateProperty? get labelTextStyle { + return WidgetStateProperty.resolveWith((Set states) { + final TextStyle style = _textTheme.labelMedium!; + return style.apply( + color: states.contains(WidgetState.disabled) + ? _colors.onSurfaceVariant.withValues(alpha: 0.38) + : states.contains(WidgetState.selected) + ? _colors.onSurface + : _colors.onSurfaceVariant, + ); + }); + } + + @override + EdgeInsetsGeometry? get labelPadding => const EdgeInsets.only(top: 2); +} diff --git a/lib/models/common/nav_bar_config.dart b/lib/models/common/nav_bar_config.dart index 920374458..75db67df5 100644 --- a/lib/models/common/nav_bar_config.dart +++ b/lib/models/common/nav_bar_config.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; enum NavigationBarType implements EnumWithLabel { home( '首页', - Icon(Icons.home_outlined, size: 23), - Icon(Icons.home, size: 21), + Icon(Icons.home_outlined, size: 24), + Icon(Icons.home, size: 24), HomePage(), ), dynamics( @@ -19,10 +19,10 @@ enum NavigationBarType implements EnumWithLabel { ), mine( '我的', - Icon(Icons.person_outline, size: 21), - Icon(Icons.person, size: 21), + Icon(Icons.person_outline, size: 24), + Icon(Icons.person, size: 24), MinePage(), - ) + ), ; @override diff --git a/lib/pages/dynamics/widgets/vote.dart b/lib/pages/dynamics/widgets/vote.dart index 452a14c03..f5d001a2b 100644 --- a/lib/pages/dynamics/widgets/vote.dart +++ b/lib/pages/dynamics/widgets/vote.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:PiliPlus/common/widgets/avatars.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 4e17eca76..203a2c17a 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -55,6 +55,7 @@ class MainController extends GetxController late int lastCheckUnreadAt = 0; final enableMYBar = Pref.enableMYBar; + final floatingNavBar = Pref.floatingNavBar; final useSideBar = Pref.useSideBar; final mainTabBarView = Pref.mainTabBarView; late final optTabletNav = Pref.optTabletNav; diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 034a974f7..8dc194877 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/floating_navigation_bar.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/flutter/tabs.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; @@ -270,67 +271,88 @@ class _MainAppState extends PopScopeState } Widget? get _bottomNav { - Widget? bottomNav = _mainController.navigationBars.length > 1 - ? _mainController.enableMYBar - ? Obx( - () => NavigationBar( - maintainBottomViewPadding: true, - onDestinationSelected: _mainController.setIndex, - selectedIndex: _mainController.selectedIndex.value, - destinations: _mainController.navigationBars - .map( - (e) => NavigationDestination( - label: e.label, - icon: _buildIcon(type: e), - selectedIcon: _buildIcon(type: e, selected: true), - ), - ) - .toList(), + Widget? bottomNav; + if (_mainController.navigationBars.length > 1) { + if (_mainController.floatingNavBar) { + bottomNav = Obx( + () => FloatingNavigationBar( + onDestinationSelected: _mainController.setIndex, + selectedIndex: _mainController.selectedIndex.value, + destinations: _mainController.navigationBars + .map( + (e) => FloatingNavigationDestination( + label: e.label, + icon: _buildIcon(type: e), + selectedIcon: _buildIcon(type: e, selected: true), ), ) - : Obx( - () => BottomNavigationBar( - currentIndex: _mainController.selectedIndex.value, - onTap: _mainController.setIndex, - iconSize: 16, - selectedFontSize: 12, - unselectedFontSize: 12, - type: .fixed, - items: _mainController.navigationBars - .map( - (e) => BottomNavigationBarItem( - label: e.label, - icon: _buildIcon(type: e), - activeIcon: _buildIcon(type: e, selected: true), - ), - ) - .toList(), + .toList(), + ), + ); + } else if (_mainController.enableMYBar) { + bottomNav = Obx( + () => NavigationBar( + maintainBottomViewPadding: true, + onDestinationSelected: _mainController.setIndex, + selectedIndex: _mainController.selectedIndex.value, + destinations: _mainController.navigationBars + .map( + (e) => NavigationDestination( + label: e.label, + icon: _buildIcon(type: e), + selectedIcon: _buildIcon(type: e, selected: true), ), ) - : null; - if (bottomNav != null && _mainController.hideBottomBar) { - if (_mainController.barOffset case final barOffset?) { - return Obx( - () => FractionalTranslation( - translation: Offset( - 0.0, - barOffset.value / Style.topBarHeight, - ), - child: bottomNav, + .toList(), + ), + ); + } else { + bottomNav = Obx( + () => BottomNavigationBar( + currentIndex: _mainController.selectedIndex.value, + onTap: _mainController.setIndex, + iconSize: 16, + selectedFontSize: 12, + unselectedFontSize: 12, + type: .fixed, + items: _mainController.navigationBars + .map( + (e) => BottomNavigationBarItem( + label: e.label, + icon: _buildIcon(type: e), + activeIcon: _buildIcon(type: e, selected: true), + ), + ) + .toList(), ), ); } - if (_mainController.showBottomBar case final showBottomBar?) { - return Obx( - () => AnimatedSlide( - curve: Curves.easeInOutCubicEmphasized, - duration: const Duration(milliseconds: 500), - offset: Offset(0, showBottomBar.value ? 0 : 1), - child: bottomNav, - ), - ); + + if (_mainController.hideBottomBar) { + if (_mainController.barOffset case final barOffset?) { + return Obx( + () => FractionalTranslation( + translation: Offset( + 0.0, + barOffset.value / Style.topBarHeight, + ), + child: bottomNav, + ), + ); + } + if (_mainController.showBottomBar case final showBottomBar?) { + return Obx( + () => AnimatedSlide( + curve: Curves.easeInOutCubicEmphasized, + duration: const Duration(milliseconds: 500), + offset: Offset(0, showBottomBar.value ? 0 : 1), + child: bottomNav, + ), + ); + } } } + return bottomNav; } diff --git a/lib/pages/setting/models/style_settings.dart b/lib/pages/setting/models/style_settings.dart index 3501a2cb4..855781421 100644 --- a/lib/pages/setting/models/style_settings.dart +++ b/lib/pages/setting/models/style_settings.dart @@ -106,7 +106,7 @@ List get styleSettings => [ ), const SwitchModel( title: '优化平板导航栏', - leading: Icon(MdiIcons.soundbar), + leading: Icon(Icons.auto_fix_high), setKey: SettingBoxKey.optTabletNav, defaultVal: true, needReboot: true, @@ -119,6 +119,13 @@ List get styleSettings => [ defaultVal: true, needReboot: true, ), + const SwitchModel( + title: '悬浮底栏', + leading: Icon(MdiIcons.soundbar), + setKey: SettingBoxKey.floatingNavBar, + defaultVal: false, + needReboot: true, + ), NormalModel( leading: const Icon(Icons.calendar_view_week_outlined), title: '列表宽度(dp)限制', diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index ae90cea50..571bd40bd 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -149,7 +149,8 @@ abstract final class SettingBoxKey { followOrderType = 'followOrderType', enableImgMenu = 'enableImgMenu', showDynDispute = 'showDynDispute', - touchSlopH = 'touchSlopH'; + touchSlopH = 'touchSlopH', + floatingNavBar = 'floatingNavBar'; static const String minimizeOnExit = 'minimizeOnExit', windowSize = 'windowSize', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 9d8dfec4b..7435667f0 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -966,4 +966,7 @@ abstract final class Pref { static bool get saveReply => _setting.get(SettingBoxKey.saveReply, defaultValue: true); + + static bool get floatingNavBar => + _setting.get(SettingBoxKey.floatingNavBar, defaultValue: false); }