diff --git a/lib/common/widgets/flutter/refresh_indicator.dart b/lib/common/widgets/flutter/refresh_indicator.dart index f1963f344..a1aebf392 100644 --- a/lib/common/widgets/flutter/refresh_indicator.dart +++ b/lib/common/widgets/flutter/refresh_indicator.dart @@ -5,9 +5,11 @@ // ignore_for_file: prefer_initializing_formals import 'dart:async' show Completer; -import 'dart:io' show Platform; import 'package:PiliPlus/common/widgets/scroll_behavior.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show BouncingScrollPhysicsExt; +import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:extended_nested_scroll_view/src/refresh.dart'; import 'package:flutter/foundation.dart' show clampDouble; @@ -543,8 +545,19 @@ class RefreshIndicatorState extends State ), ], ); - if (!widget.isClampingScrollPhysics && - (Platform.isIOS || Platform.isMacOS)) { + if (PlatformUtils.isDarwin) { + if (widget.isClampingScrollPhysics) { + return ScrollConfiguration( + behavior: RefreshScrollBehavior( + desktopDragDevices, + scrollPhysics: RefreshScrollPhysicsIOS( + parent: const RangeMaintainingScrollPhysics(), + onDrag: _onDrag, + ), + ), + child: child, + ); + } return child; } return ScrollConfiguration( @@ -640,3 +653,19 @@ class RefreshScrollPhysics extends ClampingScrollPhysics return RefreshScrollPhysics(parent: buildParent(ancestor), onDrag: onDrag); } } + +class RefreshScrollPhysicsIOS extends BouncingScrollPhysicsExt + with RefreshScrollPhysicsMixin { + const RefreshScrollPhysicsIOS({super.parent, required this.onDrag}); + + @override + final OnDrag onDrag; + + @override + RefreshScrollPhysicsIOS applyTo(ScrollPhysics? ancestor) { + return RefreshScrollPhysicsIOS( + parent: buildParent(ancestor), + onDrag: onDrag, + ); + } +} diff --git a/lib/common/widgets/scroll_physics.dart b/lib/common/widgets/scroll_physics.dart index 26358a102..823ba3640 100644 --- a/lib/common/widgets/scroll_physics.dart +++ b/lib/common/widgets/scroll_physics.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart' hide TabBarView; @@ -76,3 +77,49 @@ class ReloadScrollPhysics extends AlwaysScrollableScrollPhysics { ); } } + +final platformClampingPhysics = PlatformUtils.isDarwin + ? const BouncingScrollPhysicsExt() + : const ClampingScrollPhysics(); + +final platformAlwaysClampingPhysics = PlatformUtils.isDarwin + ? const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysicsExt()) + : const AlwaysScrollableScrollPhysics(parent: ClampingScrollPhysics()); + +class BouncingScrollPhysicsExt extends BouncingScrollPhysics + with ClampingBoundaryMixin { + const BouncingScrollPhysicsExt({super.parent}); + + @override + BouncingScrollPhysicsExt applyTo(ScrollPhysics? ancestor) { + return BouncingScrollPhysicsExt(parent: buildParent(ancestor)); + } +} + +/// [ClampingScrollPhysics.applyBoundaryConditions] +mixin ClampingBoundaryMixin on ScrollPhysics { + @override + double applyBoundaryConditions(ScrollMetrics position, double value) { + if (value < position.pixels && + position.pixels <= position.minScrollExtent) { + // Underscroll. + return value - position.pixels; + } + if (position.maxScrollExtent <= position.pixels && + position.pixels < value) { + // Overscroll. + return value - position.pixels; + } + if (value < position.minScrollExtent && + position.minScrollExtent < position.pixels) { + // Hit top edge. + return value - position.minScrollExtent; + } + if (position.pixels < position.maxScrollExtent && + position.maxScrollExtent < value) { + // Hit bottom edge. + return value - position.maxScrollExtent; + } + return 0.0; + } +} diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index 31be044f5..e73c41a82 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -9,6 +9,8 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformClampingPhysics; import 'package:PiliPlus/common/widgets/selection_text.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; @@ -918,7 +920,7 @@ class _AudioPageState extends State { child: ListView( key: const PageStorageKey(_AudioPageState), shrinkWrap: true, - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, children: [ Center( child: GestureDetector( diff --git a/lib/pages/dynamics_create/view.dart b/lib/pages/dynamics_create/view.dart index 4f1fcac79..921e1e8c3 100644 --- a/lib/pages/dynamics_create/view.dart +++ b/lib/pages/dynamics_create/view.dart @@ -8,6 +8,8 @@ import 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart' import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformClampingPhysics; import 'package:PiliPlus/common/widgets/time_picker.dart'; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -138,7 +140,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { child: ListView( padding: EdgeInsets.zero, controller: widget.scrollController, - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -630,7 +632,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { return SizedBox( height: height, child: GridView( - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, padding: const EdgeInsets.only(left: 12, bottom: 12, right: 12), gridDelegate: SliverGridDelegateWithExtentAndRatio( maxCrossAxisExtent: 65, diff --git a/lib/pages/dynamics_repost/view.dart b/lib/pages/dynamics_repost/view.dart index 123b9344f..b3ca41c90 100644 --- a/lib/pages/dynamics_repost/view.dart +++ b/lib/pages/dynamics_repost/view.dart @@ -1,6 +1,8 @@ import 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformClampingPhysics; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart'; @@ -102,7 +104,7 @@ class _RepostPanelState extends CommonRichTextPubPageState { child: ListView( padding: EdgeInsets.zero, controller: scrollController, - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, children: _buildEditPanel(theme), ), ), diff --git a/lib/pages/emote/view.dart b/lib/pages/emote/view.dart index 5cbc89cef..50e3d45bf 100644 --- a/lib/pages/emote/view.dart +++ b/lib/pages/emote/view.dart @@ -2,7 +2,8 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/custom_tooltip.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarView, platformClampingPhysics; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/emote/emote.dart'; @@ -68,7 +69,7 @@ class _EmotePanelState extends State final size = flag ? 40.0 : 60.0; final isTextEmote = e.type == 4; return GridView.builder( - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, padding: const EdgeInsets.only( left: 12, right: 12, diff --git a/lib/pages/live_emote/view.dart b/lib/pages/live_emote/view.dart index 3b92439cd..cd1f45a6d 100644 --- a/lib/pages/live_emote/view.dart +++ b/lib/pages/live_emote/view.dart @@ -3,7 +3,8 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/custom_tooltip.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarView, platformClampingPhysics; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/live/live_emote/datum.dart'; @@ -81,7 +82,7 @@ class _LiveEmotePanelState extends State final width = widthFac * 38; final height = heightFac * 38; return GridView.builder( - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, padding: const EdgeInsets.only( left: 12, right: 12, diff --git a/lib/pages/live_room/superchat/superchat_panel.dart b/lib/pages/live_room/superchat/superchat_panel.dart index 72bdd3424..d92bcaa78 100644 --- a/lib/pages/live_room/superchat/superchat_panel.dart +++ b/lib/pages/live_room/superchat/superchat_panel.dart @@ -1,3 +1,5 @@ +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformClampingPhysics; import 'package:PiliPlus/models/common/super_chat_type.dart'; import 'package:PiliPlus/pages/live_room/controller.dart'; import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart'; @@ -32,7 +34,7 @@ class _SuperChatPanelState extends DebounceStreamState () => ListView.separated( key: const PageStorageKey(_SuperChatPanelState), padding: const EdgeInsets.symmetric(horizontal: 12), - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, itemCount: widget.controller.superChatMsg.length, findItemIndexCallback: (key) { final index = widget.controller.superChatMsg.indexWhere( diff --git a/lib/pages/live_room/widgets/chat_panel.dart b/lib/pages/live_room/widgets/chat_panel.dart index 6fcbd7531..d7313734c 100644 --- a/lib/pages/live_room/widgets/chat_panel.dart +++ b/lib/pages/live_room/widgets/chat_panel.dart @@ -1,6 +1,8 @@ import 'package:PiliPlus/common/widgets/flutter/popup_menu.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformClampingPhysics; import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/models_new/live/live_danmaku/danmaku_msg.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; @@ -54,7 +56,7 @@ class LiveRoomChatPanel extends StatelessWidget { separatorBuilder: (_, _) => const SizedBox(height: 8), itemCount: liveRoomController.builtLength = liveRoomController.messages.length, - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, itemBuilder: (_, index) { final item = liveRoomController.messages[index]; if (item is DanmakuMsg) { diff --git a/lib/pages/video/reply_new/view.dart b/lib/pages/video/reply_new/view.dart index 42d8d28b7..52e8b8d40 100644 --- a/lib/pages/video/reply_new/view.dart +++ b/lib/pages/video/reply_new/view.dart @@ -7,6 +7,8 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart' show RichTextType; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformClampingPhysics; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -284,7 +286,7 @@ class _ReplyPageState extends CommonRichTextPubPageState { return SizedBox( height: height, child: GridView( - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, padding: const EdgeInsets.only(left: 12, bottom: 12, right: 12), gridDelegate: gridDelegate, children: [ diff --git a/lib/pages/video/send_danmaku/view.dart b/lib/pages/video/send_danmaku/view.dart index d34049ebd..e1c2b771e 100644 --- a/lib/pages/video/send_danmaku/view.dart +++ b/lib/pages/video/send_danmaku/view.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformClampingPhysics; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -174,7 +176,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { ), ), child: ListView( - physics: const ClampingScrollPhysics(), + physics: platformClampingPhysics, padding: .only( top: 12, bottom: 12 + MediaQuery.viewPaddingOf(context).bottom, diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index e50c96860..0cb0908da 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -8,7 +8,8 @@ import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/route_aware_mixin.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarView, platformAlwaysClampingPhysics, platformClampingPhysics; import 'package:PiliPlus/common/widgets/sliver/video_header.dart'; import 'package:PiliPlus/common/widgets/svg/play_icon.dart'; import 'package:PiliPlus/models/common/episode_panel_type.dart'; @@ -515,9 +516,10 @@ class _VideoDetailPageVState extends State ), ), body: ExtendedNestedScrollView( + onlyOneScrollInBody: true, + physics: platformClampingPhysics, key: videoDetailController.scrollKey, controller: videoDetailController.scrollCtr, - onlyOneScrollInBody: true, pinnedHeaderSliverHeightBuilder: () { double pinnedHeight = this.isFullScreen || !isPortrait ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) @@ -1616,9 +1618,7 @@ class _VideoDetailPageVState extends State controller: needCtr ? videoDetailController.effectiveIntroScrollCtr : null, - physics: !needCtr - ? const AlwaysScrollableScrollPhysics(parent: ClampingScrollPhysics()) - : null, + physics: !needCtr ? platformAlwaysClampingPhysics : null, key: const PageStorageKey(CommonIntroController), slivers: [ SliverPadding( @@ -1649,11 +1649,7 @@ class _VideoDetailPageVState extends State controller: needCtr ? videoDetailController.effectiveIntroScrollCtr : null, - physics: !needCtr - ? const AlwaysScrollableScrollPhysics( - parent: ClampingScrollPhysics(), - ) - : null, + physics: !needCtr ? platformAlwaysClampingPhysics : null, slivers: [ if (videoDetailController.isUgc) ...[ UgcIntroPanel( diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 593ae5a3a..de0ce22b4 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -7,6 +7,8 @@ import 'package:PiliPlus/common/widgets/flutter/chat_list_view.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show platformAlwaysClampingPhysics; import 'package:PiliPlus/grpc/bilibili/im/type.pb.dart' show Msg; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/msg.dart'; @@ -158,9 +160,7 @@ class _WhisperDetailPageState ? ChatListView.separated( itemCount: response.length, padding: const .all(kChatListPadding), - physics: const AlwaysScrollableScrollPhysics( - parent: ClampingScrollPhysics(), - ), + physics: platformAlwaysClampingPhysics, controller: _whisperDetailController.scrollController, itemBuilder: (context, int index) { if (index == response.length - 1) { diff --git a/lib/utils/platform_utils.dart b/lib/utils/platform_utils.dart index 0ffa1a215..9486fde35 100644 --- a/lib/utils/platform_utils.dart +++ b/lib/utils/platform_utils.dart @@ -7,4 +7,7 @@ abstract final class PlatformUtils { @pragma("vm:platform-const") static final bool isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; + + @pragma("vm:platform-const") + static final bool isDarwin = Platform.isIOS || Platform.isMacOS; }