diff --git a/lib/common/constants.dart b/lib/common/constants.dart index d27be60ec..a72f65cbf 100644 --- a/lib/common/constants.dart +++ b/lib/common/constants.dart @@ -15,6 +15,7 @@ abstract final class StyleString { minWidth: 420, maxWidth: 420, ); + static const topBarHeight = 52.0; } abstract final class Constants { diff --git a/lib/common/widgets/custom_height_widget.dart b/lib/common/widgets/custom_height_widget.dart new file mode 100644 index 000000000..e4b961028 --- /dev/null +++ b/lib/common/widgets/custom_height_widget.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show RenderProxyBox; + +class CustomHeightWidget extends SingleChildRenderObjectWidget { + const CustomHeightWidget({ + super.key, + required this.height, + this.offset = .zero, + required super.child, + }); + + final double height; + + final Offset offset; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderCustomHeightWidget( + height: height, + offset: offset, + ); + } + + @override + void updateRenderObject( + BuildContext context, + RenderCustomHeightWidget renderObject, + ) { + renderObject + ..height = height + ..offset = offset; + } +} + +class RenderCustomHeightWidget extends RenderProxyBox { + RenderCustomHeightWidget({ + required double height, + required Offset offset, + }) : _height = height, + _offset = offset; + + double _height; + double get height => _height; + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsLayout(); + } + + Offset _offset; + Offset get offset => _offset; + set offset(Offset value) { + if (_offset == value) return; + _offset = value; + markNeedsPaint(); + } + + @override + void performLayout() { + child!.layout(constraints, parentUsesSize: true); + size = constraints.constrainDimensions(constraints.maxWidth, height); + } + + @override + void paint(PaintingContext context, Offset offset) { + context.paintChild(child!, offset + _offset); + } + + @override + bool get isRepaintBoundary => true; +} diff --git a/lib/common/widgets/flutter/vertical_tabs.dart b/lib/common/widgets/flutter/vertical_tabs.dart index 29d863813..bcee9a96a 100644 --- a/lib/common/widgets/flutter/vertical_tabs.dart +++ b/lib/common/widgets/flutter/vertical_tabs.dart @@ -1653,7 +1653,7 @@ class _VerticalTabBarState extends State { tabCenter + paddingTop - viewportWidth / 2.0 + - (_mainCtr.useBottomNav && (_mainCtr.showBottomBar?.value ?? true) + (_mainCtr.useBottomNav && (_mainCtr.barOffset?.value ?? 0) == 0 ? 80.0 : 0.0), minExtent, diff --git a/lib/pages/common/common_page.dart b/lib/pages/common/common_page.dart index 6b591a35e..0d4d26556 100644 --- a/lib/pages/common/common_page.dart +++ b/lib/pages/common/common_page.dart @@ -1,7 +1,7 @@ +import 'package:PiliPlus/common/constants.dart' show StyleString; import 'package:PiliPlus/pages/common/common_controller.dart'; -import 'package:PiliPlus/pages/home/controller.dart'; import 'package:PiliPlus/pages/main/controller.dart'; -import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -12,33 +12,17 @@ abstract class CommonPageState< extends State { R get controller; final _mainController = Get.find(); - RxBool? _showBottomBar; - RxBool? _showSearchBar; - - // late double _downScrollCount = 0.0; // 向下滚动计数器 - late double _upScrollCount = 0.0; // 向上滚动计数器 - double? _lastScrollPosition; // 记录上次滚动位置 - final _enableScrollThreshold = Pref.enableScrollThreshold; - late final double _scrollThreshold = Pref.scrollThreshold; // 滚动阈值 - late final _scrollController = controller.scrollController; + RxDouble? _barOffset; @override void initState() { super.initState(); - _showBottomBar = _mainController.showBottomBar; - try { - _showSearchBar = Get.find().showSearchBar; - } catch (_) {} - if (_enableScrollThreshold && - (_showBottomBar != null || _showSearchBar != null)) { - _scrollController.addListener(listener); - } + _barOffset = _mainController.barOffset; } Widget onBuild(Widget child) { - if (!_enableScrollThreshold && - (_showBottomBar != null || _showSearchBar != null)) { - return NotificationListener( + if (_barOffset != null) { + return NotificationListener( onNotification: onNotification, child: child, ); @@ -46,69 +30,29 @@ abstract class CommonPageState< return child; } - bool onNotification(UserScrollNotification notification) { - if (notification.metrics.axis == .horizontal) return false; + bool onNotification(ScrollUpdateNotification notification) { if (!_mainController.useBottomNav) return false; - final direction = notification.direction; - if (direction == .forward) { - _showBottomBar?.value = true; - _showSearchBar?.value = true; - } else if (direction == .reverse) { - _showBottomBar?.value = false; - _showSearchBar?.value = false; + + final metrics = notification.metrics; + if (metrics.axis == .horizontal || + metrics.pixels < 0 || + notification.dragDetails == null) { + return false; } + + final scrollDelta = notification.scrollDelta ?? 0.0; + _barOffset!.value = clampDouble( + _barOffset!.value + scrollDelta, + 0.0, + StyleString.topBarHeight, + ); + return false; } - void listener() { - if (!_mainController.useBottomNav) return; - final direction = _scrollController.position.userScrollDirection; - - final double currentPosition = _scrollController.position.pixels; - - // 初始化上次位置 - _lastScrollPosition ??= currentPosition; - - // 计算滚动距离 - final double scrollDelta = currentPosition - _lastScrollPosition!; - - if (direction == .reverse) { - _showBottomBar?.value = false; - _showSearchBar?.value = false; // // 向下滚动,累加向下滚动距离,重置向上滚动计数器 - _upScrollCount = 0.0; // 重置向上滚动计数器 - // if (scrollDelta > 0) { - // _downScrollCount += scrollDelta; - // // _upScrollCount = 0.0; // 重置向上滚动计数器 - - // // 当累计向下滚动距离超过阈值时,隐藏顶底栏 - // if (_downScrollCount >= _scrollThreshold) { - // mainStream?.add(false); - // searchBarStream?.add(false); - // } - // } - } else if (direction == .forward) { - // 向上滚动,累加向上滚动距离,重置向下滚动计数器 - if (scrollDelta < 0) { - _upScrollCount -= scrollDelta; // 使用绝对值 - // _downScrollCount = 0.0; // 重置向下滚动计数器 - - // 当累计向上滚动距离超过阈值时,显示顶底栏 - if (_upScrollCount >= _scrollThreshold) { - _showBottomBar?.value = true; - _showSearchBar?.value = true; - } - } - } - - // 更新上次位置 - _lastScrollPosition = currentPosition; - } - @override void dispose() { - _showSearchBar = null; - _showBottomBar = null; - _scrollController.removeListener(listener); + _barOffset = null; super.dispose(); } } diff --git a/lib/pages/dynamics_tab/view.dart b/lib/pages/dynamics_tab/view.dart index 851a42377..e70d9ffe4 100644 --- a/lib/pages/dynamics_tab/view.dart +++ b/lib/pages/dynamics_tab/view.dart @@ -46,21 +46,13 @@ class _DynamicsTabPageState _mainController.selectedIndex.value == 0; @override - bool onNotification(UserScrollNotification notification) { + bool onNotification(ScrollUpdateNotification notification) { if (checkPage) { return false; } return super.onNotification(notification); } - @override - void listener() { - if (checkPage) { - return; - } - super.listener(); - } - @override void initState() { controller = Get.putOrFind( diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 08d658db6..680667df2 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -19,7 +19,7 @@ class HomeController extends GetxController late List tabs; late TabController tabController; - RxBool? showSearchBar; + final bool hideTopBar = !Pref.useSideBar && Pref.hideTopBar; bool enableSearchWord = Pref.enableSearchWord; late final RxString defaultSearch = ''.obs; @@ -36,10 +36,6 @@ class HomeController extends GetxController void onInit() { super.onInit(); - if (!Pref.useSideBar && Pref.hideTopBar) { - showSearchBar = true.obs; - } - if (enableSearchWord) { lateCheckSearchAt = DateTime.now().millisecondsSinceEpoch; querySearchDefault(); diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 743125b22..6fe9d8607 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/custom_height_widget.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/pages/home/controller.dart'; @@ -36,30 +37,27 @@ class _HomePageState extends State MediaQuery.sizeOf(context).isPortrait) customAppBar(theme), if (_homeController.tabs.length > 1) - Material( - color: theme.colorScheme.surface, - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: SizedBox( - height: 42, - width: double.infinity, - child: TabBar( - controller: _homeController.tabController, - tabs: _homeController.tabs - .map((e) => Tab(text: e.label)) - .toList(), - isScrollable: true, - dividerColor: Colors.transparent, - dividerHeight: 0, - splashBorderRadius: StyleString.mdRadius, - tabAlignment: TabAlignment.center, - onTap: (_) { - feedBack(); - if (!_homeController.tabController.indexIsChanging) { - _homeController.animateToTop(); - } - }, - ), + Padding( + padding: const EdgeInsets.only(top: 4), + child: SizedBox( + height: 42, + width: double.infinity, + child: TabBar( + controller: _homeController.tabController, + tabs: _homeController.tabs + .map((e) => Tab(text: e.label)) + .toList(), + isScrollable: true, + dividerColor: Colors.transparent, + dividerHeight: 0, + splashBorderRadius: StyleString.mdRadius, + tabAlignment: TabAlignment.center, + onTap: (_) { + feedBack(); + if (!_homeController.tabController.indexIsChanging) { + _homeController.animateToTop(); + } + }, ), ), ) @@ -76,7 +74,6 @@ class _HomePageState extends State } Widget customAppBar(ThemeData theme) { - const height = 52.0; const padding = EdgeInsets.fromLTRB(14, 6, 14, 0); final child = Row( children: [ @@ -87,24 +84,23 @@ class _HomePageState extends State userAvatar(theme: theme, mainController: _mainController), ], ); - if (_homeController.showSearchBar case final searchBar?) { - return Obx(() { - final showSearchBar = searchBar.value; - return AnimatedOpacity( - opacity: showSearchBar ? 1 : 0, - duration: const Duration(milliseconds: 300), - child: AnimatedContainer( - curve: Curves.easeInOutCubicEmphasized, - duration: const Duration(milliseconds: 500), - height: showSearchBar ? height : 0, - padding: padding, - child: child, - ), - ); - }); + if (_homeController.hideTopBar) { + return Obx( + () { + final barOffset = _mainController.barOffset!.value; + return CustomHeightWidget( + offset: Offset(0, -barOffset), + height: StyleString.topBarHeight - barOffset, + child: Padding( + padding: padding, + child: child, + ), + ); + }, + ); } else { return Container( - height: height, + height: StyleString.topBarHeight, padding: padding, child: child, ); diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 571ad1ed1..09ae2d41c 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -30,7 +30,9 @@ class MainController extends GetxController List navigationBars = []; - RxBool? showBottomBar; + RxDouble? barOffset; + late final bool hideBottomBar; + late double navHeight = 80.0; bool useBottomNav = false; late dynamic controller; final RxInt selectedIndex = 0.obs; @@ -82,9 +84,12 @@ class MainController extends GetxController ) : PageController(initialPage: selectedIndex.value); - if (!useSideBar && navigationBars.length > 1 && Pref.hideBottomBar) { - showBottomBar = true.obs; + hideBottomBar = + !useSideBar && navigationBars.length > 1 && Pref.hideBottomBar; + if (hideBottomBar || homeController.hideTopBar) { + barOffset = RxDouble(0.0); } + dynamicBadgeMode = Pref.dynamicBadgeMode; hasDyn = navigationBars.contains(NavigationBarType.dynamics); @@ -313,15 +318,9 @@ class MainController extends GetxController } } - void setSearchBar() { - if (hasHome) { - homeController.showSearchBar?.value = true; - } - } - @override void onClose() { - showBottomBar?.close(); + barOffset?.close(); controller.dispose(); super.onClose(); } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index aa0f46532..0aa3993c4 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/custom_height_widget.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'; @@ -35,6 +36,7 @@ class _MainAppState extends PopScopeState with RouteAware, WidgetsBindingObserver, WindowListener, TrayListener { final _mainController = Get.put(MainController()); late final _setting = GStorage.setting; + late EdgeInsets _padding; @override void initState() { @@ -54,6 +56,8 @@ class _MainAppState extends PopScopeState @override void didChangeDependencies() { super.didChangeDependencies(); + _padding = MediaQuery.viewPaddingOf(context); + _mainController.navHeight = 80.0 + _padding.bottom; final brightness = Theme.brightnessOf(context); NetworkImgLayer.reduce = NetworkImgLayer.reduceLuxColor != null && brightness.isDark; @@ -249,8 +253,7 @@ class _MainAppState extends PopScopeState if (_mainController.selectedIndex.value != 0) { _mainController ..setIndex(0) - ..showBottomBar?.value = true - ..setSearchBar(); + ..barOffset?.value = 0.0; } else { _onBack(); } @@ -297,12 +300,12 @@ class _MainAppState extends PopScopeState ) : null; if (bottomNav != null) { - if (_mainController.showBottomBar case final bottomBar?) { + if (_mainController.barOffset case final bottomBarOffset?) { return Obx( - () => AnimatedSlide( - curve: Curves.easeInOutCubicEmphasized, - duration: const Duration(milliseconds: 500), - offset: Offset(0, bottomBar.value ? 0 : 1), + () => CustomHeightWidget( + height: + _mainController.navHeight * + (1 - bottomBarOffset.value / StyleString.topBarHeight), child: bottomNav, ), ); @@ -381,8 +384,6 @@ class _MainAppState extends PopScopeState @override Widget build(BuildContext context) { final theme = Theme.of(context); - final padding = MediaQuery.viewPaddingOf(context); - Widget child; if (_mainController.mainTabBarView) { child = CustomTabBarView( @@ -409,7 +410,7 @@ class _MainAppState extends PopScopeState _sideBar(theme), VerticalDivider( width: 1, - endIndent: padding.bottom, + endIndent: _padding.bottom, color: theme.colorScheme.outline.withValues(alpha: 0.06), ), Expanded(child: child), @@ -423,8 +424,8 @@ class _MainAppState extends PopScopeState appBar: AppBar(toolbarHeight: 0), body: Padding( padding: EdgeInsets.only( - left: _mainController.useBottomNav ? padding.left : 0.0, - right: padding.right, + left: _mainController.useBottomNav ? _padding.left : 0.0, + right: _padding.right, ), child: child, ), diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 1c582ebc9..b6ee8ff23 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -45,21 +45,13 @@ class _MediaPageState extends CommonPageState _mainController.selectedIndex.value == 0; @override - bool onNotification(UserScrollNotification notification) { + bool onNotification(ScrollUpdateNotification notification) { if (checkPage) { return false; } return super.onNotification(notification); } - @override - void listener() { - if (checkPage) { - return; - } - super.listener(); - } - @override Widget build(BuildContext context) { super.build(context); diff --git a/lib/pages/setting/models/style_settings.dart b/lib/pages/setting/models/style_settings.dart index 69089ad20..084223723 100644 --- a/lib/pages/setting/models/style_settings.dart +++ b/lib/pages/setting/models/style_settings.dart @@ -194,15 +194,6 @@ List get styleSettings => [ defaultVal: PlatformUtils.isMobile, needReboot: true, ), - const SwitchModel( - title: '顶/底栏滚动阈值', - subtitle: '滚动多少像素后收起/展开顶底栏,默认50像素', - leading: Icon(Icons.swipe_vertical), - defaultVal: false, - setKey: SettingBoxKey.enableScrollThreshold, - needReboot: true, - onTap: _showScrollDialog, - ), NormalModel( onTap: (context, setState) => _showQualityDialog( context: context, @@ -781,48 +772,6 @@ Future _showMsgUnReadDialog( } } -void _showScrollDialog(BuildContext context) { - String scrollThreshold = Pref.scrollThreshold.toString(); - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('滚动阈值'), - content: TextFormField( - autofocus: true, - initialValue: scrollThreshold, - keyboardType: const .numberWithOptions(decimal: true), - onChanged: (value) => scrollThreshold = value, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), - ], - decoration: const InputDecoration(suffixText: 'px'), - ), - actions: [ - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle(color: ColorScheme.of(context).outline), - ), - ), - TextButton( - onPressed: () { - try { - final val = double.parse(scrollThreshold); - Get.back(); - GStorage.setting.put(SettingBoxKey.scrollThreshold, val); - SmartDialog.showToast('重启生效'); - } catch (e) { - SmartDialog.showToast(e.toString()); - } - }, - child: const Text('确定'), - ), - ], - ), - ); -} - void _showReduceColorDialog( BuildContext context, VoidCallback setState, diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index fa3d77c03..2118b9344 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -220,8 +220,6 @@ abstract final class SettingBoxKey { enableMYBar = 'enableMYBar', hideTopBar = 'hideSearchBar', hideBottomBar = 'hideTabBar', - scrollThreshold = 'scrollThreshold', - enableScrollThreshold = 'enableScrollThreshold', tabBarSort = 'tabBarSort', dynamicBadgeMode = 'dynamicBadgeMode', msgBadgeMode = 'msgBadgeMode', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index c5838d3f1..08b25b9cc 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -672,12 +672,6 @@ abstract final class Pref { defaultValue: PlatformUtils.isMobile, ); - static bool get enableScrollThreshold => - _setting.get(SettingBoxKey.enableScrollThreshold, defaultValue: false); - - static double get scrollThreshold => - _setting.get(SettingBoxKey.scrollThreshold, defaultValue: 50.0); - static bool get enableSearchWord => _setting.get(SettingBoxKey.enableSearchWord, defaultValue: false);