mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
Add configurable scroll threshold (#910)
* Add configurable scroll threshold * update --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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,15 +36,58 @@ abstract class CommonPageState<T extends CommonPage, R extends CommonController>
|
||||
}
|
||||
|
||||
void listener() {
|
||||
final ScrollDirection direction =
|
||||
controller.scrollController.position.userScrollDirection;
|
||||
if (direction == ScrollDirection.forward) {
|
||||
mainStream?.add(true);
|
||||
searchBarStream?.add(true);
|
||||
} else if (direction == ScrollDirection.reverse) {
|
||||
mainStream?.add(false);
|
||||
searchBarStream?.add(false);
|
||||
final scrollController = controller.scrollController;
|
||||
final direction = scrollController.position.userScrollDirection;
|
||||
|
||||
if (!_enableScrollThreshold) {
|
||||
if (direction == ScrollDirection.forward) {
|
||||
mainStream?.add(true);
|
||||
searchBarStream?.add(true);
|
||||
} 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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
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: bottomNav,
|
||||
);
|
||||
},
|
||||
)
|
||||
: bottomNav
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
onChanged: (value) {
|
||||
try {
|
||||
Get.find<MainController>().navSearchStreamDebounce = value;
|
||||
Get.forceAppUpdate();
|
||||
} catch (_) {}
|
||||
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) {
|
||||
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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user