opt hide top/bottom bar

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-02-18 14:48:42 +08:00
parent a5efca4e1f
commit dfa258b9e6
13 changed files with 157 additions and 224 deletions

View File

@@ -15,6 +15,7 @@ abstract final class StyleString {
minWidth: 420,
maxWidth: 420,
);
static const topBarHeight = 52.0;
}
abstract final class Constants {

View File

@@ -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;
}

View File

@@ -1653,7 +1653,7 @@ class _VerticalTabBarState extends State<VerticalTabBar> {
tabCenter +
paddingTop -
viewportWidth / 2.0 +
(_mainCtr.useBottomNav && (_mainCtr.showBottomBar?.value ?? true)
(_mainCtr.useBottomNav && (_mainCtr.barOffset?.value ?? 0) == 0
? 80.0
: 0.0),
minExtent,

View File

@@ -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<T> {
R get controller;
final _mainController = Get.find<MainController>();
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<HomeController>().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<UserScrollNotification>(
if (_barOffset != null) {
return NotificationListener<ScrollUpdateNotification>(
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();
}
}

View File

@@ -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(

View File

@@ -19,7 +19,7 @@ class HomeController extends GetxController
late List<HomeTabType> 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();

View File

@@ -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<HomePage>
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<HomePage>
}
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<HomePage>
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,
);

View File

@@ -30,7 +30,9 @@ class MainController extends GetxController
List<NavigationBarType> navigationBars = <NavigationBarType>[];
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();
}

View File

@@ -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<MainApp>
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<MainApp>
@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<MainApp>
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<MainApp>
)
: 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<MainApp>
@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<MainApp>
_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<MainApp>
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,
),

View File

@@ -45,21 +45,13 @@ class _MediaPageState extends CommonPageState<MinePage, MineController>
_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);

View File

@@ -194,15 +194,6 @@ List<SettingsModel> 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<void> _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,

View File

@@ -220,8 +220,6 @@ abstract final class SettingBoxKey {
enableMYBar = 'enableMYBar',
hideTopBar = 'hideSearchBar',
hideBottomBar = 'hideTabBar',
scrollThreshold = 'scrollThreshold',
enableScrollThreshold = 'enableScrollThreshold',
tabBarSort = 'tabBarSort',
dynamicBadgeMode = 'dynamicBadgeMode',
msgBadgeMode = 'msgBadgeMode',

View File

@@ -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);