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/common/common_controller.dart';
import 'package:PiliPlus/pages/home/controller.dart'; import 'package:PiliPlus/pages/home/controller.dart';
import 'package:PiliPlus/pages/main/controller.dart'; import 'package:PiliPlus/pages/main/controller.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -16,6 +17,11 @@ abstract class CommonPageState<T extends CommonPage, R extends CommonController>
R get controller; R get controller;
StreamController<bool>? mainStream; StreamController<bool>? mainStream;
StreamController<bool>? searchBarStream; 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 @override
void initState() { void initState() {
@@ -30,15 +36,58 @@ abstract class CommonPageState<T extends CommonPage, R extends CommonController>
} }
void listener() { void listener() {
final ScrollDirection direction = final scrollController = controller.scrollController;
controller.scrollController.position.userScrollDirection; final direction = scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream?.add(true); if (!_enableScrollThreshold) {
searchBarStream?.add(true); if (direction == ScrollDirection.forward) {
} else if (direction == ScrollDirection.reverse) { mainStream?.add(true);
mainStream?.add(false); searchBarStream?.add(true);
searchBarStream?.add(false); } else if (direction == ScrollDirection.reverse) {
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 @override

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:PiliPlus/common/widgets/custom_toast.dart'; import 'package:PiliPlus/common/widgets/custom_toast.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
@@ -343,15 +344,61 @@ List<SettingsModel> get styleSettings => [
), ),
SettingsModel( SettingsModel(
settingsType: SettingsType.sw1tch, settingsType: SettingsType.sw1tch,
title: '降低收起/展开顶/底栏频率', title: '顶/底栏滚动阈值',
leading: const Icon(Icons.vertical_distribute), subtitle: '滚动多少像素后收起/展开顶底栏默认50像素',
setKey: SettingBoxKey.navSearchStreamDebounce, leading: const Icon(Icons.swipe_vertical),
defaultVal: false, defaultVal: true,
onChanged: (value) { setKey: SettingBoxKey.enableScrollThreshold,
try { needReboot: true,
Get.find<MainController>().navSearchStreamDebounce = value; onTap: () {
Get.forceAppUpdate(); String scrollThreshold = Pref.scrollThreshold.toString();
} catch (_) {} 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) {
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( SettingsModel(

View File

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

View File

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

View File

@@ -502,9 +502,6 @@ class Pref {
static bool get recordSearchHistory => static bool get recordSearchHistory =>
_setting.get(SettingBoxKey.recordSearchHistory, defaultValue: true); _setting.get(SettingBoxKey.recordSearchHistory, defaultValue: true);
static bool get navSearchStreamDebounce =>
_setting.get(SettingBoxKey.navSearchStreamDebounce, defaultValue: false);
static String get webdavUri => static String get webdavUri =>
_setting.get(SettingBoxKey.webdavUri, defaultValue: ''); _setting.get(SettingBoxKey.webdavUri, defaultValue: '');
@@ -607,6 +604,12 @@ class Pref {
static bool get hideSearchBar => static bool get hideSearchBar =>
_setting.get(SettingBoxKey.hideSearchBar, defaultValue: true); _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 => static bool get enableSearchWord =>
_setting.get(SettingBoxKey.enableSearchWord, defaultValue: true); _setting.get(SettingBoxKey.enableSearchWord, defaultValue: true);