Add configurable scroll threshold (#910)

* Add configurable scroll threshold

* update

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
Tong xuewen
2025-07-29 23:02:05 +08:00
committed by GitHub
parent cf403aaf78
commit 3eb9c5b8ba
8 changed files with 191 additions and 103 deletions

View File

@@ -3,6 +3,7 @@ import 'dart:async';
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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@@ -16,6 +17,11 @@ abstract class CommonPageState<T extends CommonPage, R extends CommonController>
R get controller;
StreamController<bool>? mainStream;
StreamController<bool>? searchBarStream;
// late double _downScrollCount = 0.0; // 向下滚动计数器
late double _upScrollCount = 0.0; // 向上滚动计数器
double? _lastScrollPosition; // 记录上次滚动位置
final _enableScrollThreshold = Pref.enableScrollThreshold;
late final double _scrollThreshold = Pref.scrollThreshold; // 滚动阈值
@override
void initState() {
@@ -30,8 +36,10 @@ abstract class CommonPageState<T extends CommonPage, R extends CommonController>
}
void listener() {
final ScrollDirection direction =
controller.scrollController.position.userScrollDirection;
final scrollController = controller.scrollController;
final direction = scrollController.position.userScrollDirection;
if (!_enableScrollThreshold) {
if (direction == ScrollDirection.forward) {
mainStream?.add(true);
searchBarStream?.add(true);
@@ -39,6 +47,47 @@ abstract class CommonPageState<T extends CommonPage, R extends CommonController>
mainStream?.add(false);
searchBarStream?.add(false);
}
return;
}
final double currentPosition = scrollController.position.pixels;
// 初始化上次位置
_lastScrollPosition ??= currentPosition;
// 计算滚动距离
final double scrollDelta = currentPosition - _lastScrollPosition!;
if (direction == ScrollDirection.reverse) {
mainStream?.add(false);
searchBarStream?.add(false); // // 向下滚动,累加向下滚动距离,重置向上滚动计数器
_upScrollCount = 0.0; // 重置向上滚动计数器
// if (scrollDelta > 0) {
// _downScrollCount += scrollDelta;
// // _upScrollCount = 0.0; // 重置向上滚动计数器
// // 当累计向下滚动距离超过阈值时,隐藏顶底栏
// if (_downScrollCount >= _scrollThreshold) {
// mainStream?.add(false);
// searchBarStream?.add(false);
// }
// }
} else if (direction == ScrollDirection.forward) {
// 向上滚动,累加向上滚动距离,重置向下滚动计数器
if (scrollDelta < 0) {
_upScrollCount += (-scrollDelta); // 使用绝对值
// _downScrollCount = 0.0; // 重置向下滚动计数器
// 当累计向上滚动距离超过阈值时,显示顶底栏
if (_upScrollCount >= _scrollThreshold) {
mainStream?.add(true);
searchBarStream?.add(true);
}
}
}
// 更新上次位置
_lastScrollPosition = currentPosition;
}
@override

View File

@@ -10,7 +10,6 @@ import 'package:PiliPlus/utils/feed_back.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:stream_transform/stream_transform.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@@ -149,14 +148,11 @@ class _HomePageState extends State<HomePage>
}
Widget customAppBar(ThemeData theme) {
if (!_homeController.hideSearchBar) {
return searchBarAndUser(theme);
}
return StreamBuilder(
stream: _homeController.hideSearchBar
? _mainController.navSearchStreamDebounce
? _homeController.searchBarStream?.stream.distinct().throttle(
const Duration(milliseconds: 500),
)
: _homeController.searchBarStream?.stream.distinct()
: null,
stream: _homeController.searchBarStream?.stream.distinct(),
initialData: true,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return AnimatedOpacity(

View File

@@ -52,7 +52,6 @@ class MainController extends GetxController
final enableMYBar = Pref.enableMYBar;
final useSideBar = Pref.useSideBar;
final mainTabBarView = Pref.mainTabBarView;
late bool navSearchStreamDebounce = Pref.navSearchStreamDebounce;
late final optTabletNav = Pref.optTabletNav;
late bool directExitOnBack = Pref.directExitOnBack;

View File

@@ -16,7 +16,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:stream_transform/stream_transform.dart';
class MainApp extends StatefulWidget {
const MainApp({super.key});
@@ -91,6 +90,52 @@ class _MainAppState extends State<MainApp>
Widget build(BuildContext context) {
final theme = Theme.of(context);
final bool isPortrait = context.orientation == Orientation.portrait;
final useBottomNav = isPortrait && !_mainController.useSideBar;
Widget? bottomNav = useBottomNav
? _mainController.navigationBars.length > 1
? _mainController.enableMYBar
? Obx(
() => NavigationBar(
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(),
),
)
: Obx(
() => BottomNavigationBar(
currentIndex: _mainController.selectedIndex.value,
onTap: _mainController.setIndex,
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
type: BottomNavigationBarType.fixed,
items: _mainController.navigationBars
.map(
(e) => BottomNavigationBarItem(
label: e.label,
icon: _buildIcon(type: e),
activeIcon: _buildIcon(
type: e,
selected: true,
),
),
)
.toList(),
),
)
: const SizedBox.shrink()
: null;
return PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
@@ -120,7 +165,7 @@ class _MainAppState extends State<MainApp>
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_mainController.useSideBar || !isPortrait) ...[
if (!useBottomNav) ...[
_mainController.navigationBars.length > 1
? context.isTablet && _mainController.optTabletNav
? Column(
@@ -228,74 +273,23 @@ class _MainAppState extends State<MainApp>
],
),
),
bottomNavigationBar: _mainController.useSideBar || !isPortrait
? null
: StreamBuilder(
stream: _mainController.hideTabBar
? _mainController.navSearchStreamDebounce
? _mainController.bottomBarStream?.stream
.distinct()
.throttle(const Duration(milliseconds: 500))
: _mainController.bottomBarStream?.stream.distinct()
: null,
bottomNavigationBar: useBottomNav
? _mainController.hideTabBar
? StreamBuilder(
stream: _mainController.bottomBarStream?.stream
.distinct(),
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
child: _mainController.enableMYBar
? _mainController.navigationBars.length > 1
? Obx(
() => NavigationBar(
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(),
),
)
: const SizedBox.shrink()
: _mainController.navigationBars.length > 1
? Obx(
() => BottomNavigationBar(
currentIndex:
_mainController.selectedIndex.value,
onTap: _mainController.setIndex,
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
type: BottomNavigationBarType.fixed,
items: _mainController.navigationBars
.map(
(e) => BottomNavigationBarItem(
label: e.label,
icon: _buildIcon(type: e),
activeIcon: _buildIcon(
type: e,
selected: true,
),
),
)
.toList(),
),
)
: const SizedBox.shrink(),
child: bottomNav,
);
},
),
)
: bottomNav
: null,
),
),
);

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:PiliPlus/common/widgets/custom_toast.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
@@ -343,15 +344,61 @@ List<SettingsModel> get styleSettings => [
),
SettingsModel(
settingsType: SettingsType.sw1tch,
title: '降低收起/展开顶/底栏频率',
leading: const Icon(Icons.vertical_distribute),
setKey: SettingBoxKey.navSearchStreamDebounce,
defaultVal: false,
title: '顶/底栏滚动阈值',
subtitle: '滚动多少像素后收起/展开顶底栏默认50像素',
leading: const Icon(Icons.swipe_vertical),
defaultVal: true,
setKey: SettingBoxKey.enableScrollThreshold,
needReboot: true,
onTap: () {
String scrollThreshold = Pref.scrollThreshold.toString();
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('滚动阈值'),
content: TextFormField(
autofocus: true,
initialValue: scrollThreshold,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
onChanged: (value) {
try {
Get.find<MainController>().navSearchStreamDebounce = value;
Get.forceAppUpdate();
} catch (_) {}
scrollThreshold = value;
},
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')),
],
decoration: const InputDecoration(suffixText: 'px'),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
GStorage.setting.put(
SettingBoxKey.scrollThreshold,
max(
10.0,
double.tryParse(scrollThreshold) ?? 50.0,
),
);
SmartDialog.showToast('重启生效');
},
child: const Text('确定'),
),
],
);
},
);
},
),
SettingsModel(

View File

@@ -131,9 +131,8 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
return ListTile(
contentPadding: widget.contentPadding,
enabled: widget.onTap != null ? val : true,
onTap: () => widget.onTap != null
? widget.onTap?.call()
: switchChange(theme, null),
onTap: () =>
widget.onTap != null ? widget.onTap!() : switchChange(theme, null),
title: Text(widget.title!, style: titleStyle),
subtitle: widget.subtitle != null
? Text(widget.subtitle!, style: subTitleStyle)

View File

@@ -123,7 +123,6 @@ class SettingBoxKey {
appFontWeight = 'appFontWeight',
fastForBackwardDuration = 'fastForBackwardDuration',
recordSearchHistory = 'recordSearchHistory',
navSearchStreamDebounce = 'navSearchStreamDebounce',
showPgcTimeline = 'showPgcTimeline',
pageTransition = 'pageTransition',
optTabletNav = 'optTabletNav',
@@ -194,6 +193,8 @@ class SettingBoxKey {
enableMYBar = 'enableMYBar',
hideSearchBar = 'hideSearchBar',
hideTabBar = 'hideTabBar',
scrollThreshold = 'scrollThreshold',
enableScrollThreshold = 'enableScrollThreshold',
tabBarSort = 'tabBarSort',
dynamicBadgeMode = 'dynamicBadgeMode',
msgBadgeMode = 'msgBadgeMode',

View File

@@ -502,9 +502,6 @@ class Pref {
static bool get recordSearchHistory =>
_setting.get(SettingBoxKey.recordSearchHistory, defaultValue: true);
static bool get navSearchStreamDebounce =>
_setting.get(SettingBoxKey.navSearchStreamDebounce, defaultValue: false);
static String get webdavUri =>
_setting.get(SettingBoxKey.webdavUri, defaultValue: '');
@@ -607,6 +604,12 @@ class Pref {
static bool get hideSearchBar =>
_setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
static bool get enableScrollThreshold =>
_setting.get(SettingBoxKey.enableScrollThreshold, defaultValue: true);
static double get scrollThreshold =>
_setting.get(SettingBoxKey.scrollThreshold, defaultValue: 50.0);
static bool get enableSearchWord =>
_setting.get(SettingBoxKey.enableSearchWord, defaultValue: true);