mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-01 00:28:18 +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/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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user