From 7e9da945f6fda8965b1c09142f491e50efc28030 Mon Sep 17 00:00:00 2001 From: Thomas <9749173+uhthomas@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:28:55 +0000 Subject: [PATCH] chore(mobile): simplify asset page scroll (#26635) In order to scroll smoothly without interfering with the gesture detector on the photo view, we have an offstate scroll view which we defer all drags to, and then forward scroll offsets to the real scroll controller. This works well, but it can be simpler. Instead, we can create a custom scroll controller on a scroll view with never scrollable physics, and then forward drag events to that, bypassing the need for a proxy scroll controller. Co-authored-by: Alex --- mobile/lib/extensions/scroll_extensions.dart | 109 +++++------------- .../asset_viewer/asset_page.widget.dart | 47 +++----- 2 files changed, 46 insertions(+), 110 deletions(-) diff --git a/mobile/lib/extensions/scroll_extensions.dart b/mobile/lib/extensions/scroll_extensions.dart index 5917e127bc..5b8f9e2a13 100644 --- a/mobile/lib/extensions/scroll_extensions.dart +++ b/mobile/lib/extensions/scroll_extensions.dart @@ -33,12 +33,27 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics { ); } +class SnapScrollController extends ScrollController { + SnapScrollPosition get snapPosition => position as SnapScrollPosition; + + @override + ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) => + SnapScrollPosition(physics: physics, context: context, oldPosition: oldPosition); +} + +class SnapScrollPosition extends ScrollPositionWithSingleContext { + double snapOffset; + + SnapScrollPosition({required super.physics, required super.context, super.oldPosition, this.snapOffset = 0.0}); + + @override + bool get shouldIgnorePointer => false; +} + class SnapScrollPhysics extends ScrollPhysics { static const _minFlingVelocity = 700.0; static const minSnapDistance = 30.0; - static final _spring = SpringDescription.withDampingRatio(mass: .5, stiffness: 300); - const SnapScrollPhysics({super.parent}); @override @@ -66,91 +81,21 @@ class SnapScrollPhysics extends ScrollPhysics { } } - return ScrollSpringSimulation( - _spring, - position.pixels, - target(position, velocity, snapOffset), - velocity, - tolerance: toleranceFor(position), - ); + return ScrollSpringSimulation(spring, position.pixels, target(position, velocity, snapOffset), velocity); } + @override + SpringDescription get spring => SpringDescription.withDampingRatio(mass: .5, stiffness: 300); + + @override + bool get allowImplicitScrolling => false; + + @override + bool get allowUserScrolling => false; + static double target(ScrollMetrics position, double velocity, double snapOffset) { if (velocity > _minFlingVelocity) return snapOffset; if (velocity < -_minFlingVelocity) return position.pixels < snapOffset ? 0.0 : snapOffset; return position.pixels < minSnapDistance ? 0.0 : snapOffset; } } - -class SnapScrollPosition extends ScrollPositionWithSingleContext { - double snapOffset; - - SnapScrollPosition({this.snapOffset = 0.0, required super.physics, required super.context, super.oldPosition}); -} - -class ProxyScrollController extends ScrollController { - final ScrollController scrollController; - - ProxyScrollController({required this.scrollController}); - - SnapScrollPosition get snapPosition => position as SnapScrollPosition; - - @override - ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) { - return ProxyScrollPosition( - scrollController: scrollController, - physics: physics, - context: context, - oldPosition: oldPosition, - ); - } - - @override - void dispose() { - scrollController.dispose(); - super.dispose(); - } -} - -class ProxyScrollPosition extends SnapScrollPosition { - final ScrollController scrollController; - - ProxyScrollPosition({ - required this.scrollController, - required super.physics, - required super.context, - super.oldPosition, - }); - - @override - double setPixels(double newPixels) { - final overscroll = super.setPixels(newPixels); - if (scrollController.hasClients && scrollController.position.pixels != pixels) { - scrollController.position.forcePixels(pixels); - } - return overscroll; - } - - @override - void forcePixels(double value) { - super.forcePixels(value); - if (scrollController.hasClients && scrollController.position.pixels != pixels) { - scrollController.position.forcePixels(pixels); - } - } - - @override - double get maxScrollExtent => scrollController.hasClients && scrollController.position.hasContentDimensions - ? scrollController.position.maxScrollExtent - : super.maxScrollExtent; - - @override - double get minScrollExtent => scrollController.hasClients && scrollController.position.hasContentDimensions - ? scrollController.position.minScrollExtent - : super.minScrollExtent; - - @override - double get viewportDimension => scrollController.hasClients && scrollController.position.hasViewportDimension - ? scrollController.position.viewportDimension - : super.viewportDimension; -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index 5da8227ef0..ea7ff51fa6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -50,8 +50,7 @@ class _AssetPageState extends ConsumerState { bool _showingDetails = false; bool _isZoomed = false; - final _scrollController = ScrollController(); - late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController); + final _scrollController = SnapScrollController(); double _snapOffset = 0.0; DragStartDetails? _dragStart; @@ -63,17 +62,17 @@ class _AssetPageState extends ConsumerState { super.initState(); _eventSubscription = EventStream.shared.listen(_onEvent); WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted || !_proxyScrollController.hasClients) return; - _proxyScrollController.snapPosition.snapOffset = _snapOffset; + if (!mounted || !_scrollController.hasClients) return; + _scrollController.snapPosition.snapOffset = _snapOffset; if (_showingDetails && _snapOffset > 0) { - _proxyScrollController.jumpTo(_snapOffset); + _scrollController.jumpTo(_snapOffset); } }); } @override void dispose() { - _proxyScrollController.dispose(); + _scrollController.dispose(); _scaleBoundarySub?.cancel(); _eventSubscription?.cancel(); super.dispose(); @@ -88,21 +87,20 @@ class _AssetPageState extends ConsumerState { } void _showDetails() { - if (!_proxyScrollController.hasClients || _snapOffset <= 0) return; + if (!_scrollController.hasClients || _snapOffset <= 0) return; _viewer.setShowingDetails(true); - _proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic); + _scrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic); } - bool _willClose(double scrollVelocity) { - if (!_proxyScrollController.hasClients || _snapOffset <= 0) return false; - - final position = _proxyScrollController.position; - return _proxyScrollController.position.pixels < _snapOffset && - SnapScrollPhysics.target(position, scrollVelocity, _snapOffset) < SnapScrollPhysics.minSnapDistance; - } + bool _willClose(double scrollVelocity) => + _scrollController.hasClients && + _snapOffset > 0 && + _scrollController.position.pixels < _snapOffset && + SnapScrollPhysics.target(_scrollController.position, scrollVelocity, _snapOffset) < + SnapScrollPhysics.minSnapDistance; void _syncShowingDetails() { - final offset = _proxyScrollController.offset; + final offset = _scrollController.offset; if (offset > SnapScrollPhysics.minSnapDistance) { _viewer.setShowingDetails(true); } else if (offset < SnapScrollPhysics.minSnapDistance - kTouchSlop) { @@ -124,8 +122,8 @@ class _AssetPageState extends ConsumerState { } void _startProxyDrag() { - if (_proxyScrollController.hasClients && _dragStart != null) { - _drag = _proxyScrollController.position.drag(_dragStart!, () => _drag = null); + if (_scrollController.hasClients && _dragStart != null) { + _drag = _scrollController.position.drag(_dragStart!, () => _drag = null); } } @@ -390,22 +388,15 @@ class _AssetPageState extends ConsumerState { _snapOffset = detailsOffset - snapTarget; - if (_proxyScrollController.hasClients) { - _proxyScrollController.snapPosition.snapOffset = _snapOffset; + if (_scrollController.hasClients) { + _scrollController.snapPosition.snapOffset = _snapOffset; } return Stack( children: [ - Offstage( - child: SingleChildScrollView( - controller: _proxyScrollController, - physics: const SnapScrollPhysics(), - child: const SizedBox.shrink(), - ), - ), SingleChildScrollView( controller: _scrollController, - physics: const NeverScrollableScrollPhysics(), + physics: const SnapScrollPhysics(), child: Stack( children: [ SizedBox(