From 6b3e07dc52a8b91d2a13c047d1c1aa066a731b77 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:02:01 +0200 Subject: [PATCH 1/2] update animation methods to return futures --- .../src/controller/photo_view_controller.dart | 35 ++++++++-------- .../photo_view/src/core/photo_view_core.dart | 40 +++++++++---------- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart index b9475a9ee2..c86591fc83 100644 --- a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart +++ b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart @@ -38,12 +38,13 @@ abstract class PhotoViewControllerBase { /// Closes streams and removes eventual listeners. void dispose(); - void positionAnimationBuilder(void Function(Offset)? value); - void scaleAnimationBuilder(void Function(double)? value); - void rotationAnimationBuilder(void Function(double)? value); + void positionAnimationBuilder(Future Function(Offset)? value); + void scaleAnimationBuilder(Future Function(double)? value); + void rotationAnimationBuilder(Future Function(double)? value); - /// Animates multiple fields of the state - void animateMultiple({Offset? position, double? scale, double? rotation}); + /// Animates multiple fields of the state. The returned future completes + /// when all underlying animations have settled. + Future animateMultiple({Offset? position, double? scale, double? rotation}); /// Add a listener that will ignore updates made internally /// @@ -148,9 +149,9 @@ class PhotoViewController implements PhotoViewControllerBase Function(Offset)? _animatePosition; + late Future Function(double)? _animateScale; + late Future Function(double)? _animateRotation; @override Stream get outputStateStream => _outputCtrl.stream; @@ -159,17 +160,17 @@ class PhotoViewController implements PhotoViewControllerBase Function(Offset)? value) { _animatePosition = value; } @override - void scaleAnimationBuilder(void Function(double)? value) { + void scaleAnimationBuilder(Future Function(double)? value) { _animateScale = value; } @override - void rotationAnimationBuilder(void Function(double)? value) { + void rotationAnimationBuilder(Future Function(double)? value) { _animateRotation = value; } @@ -193,18 +194,18 @@ class PhotoViewController implements PhotoViewControllerBase animateMultiple({Offset? position, double? scale, double? rotation}) { + final futures = >[]; if (position != null && _animatePosition != null) { - _animatePosition!(position); + futures.add(_animatePosition!(position)); } - if (scale != null && _animateScale != null) { - _animateScale!(scale); + futures.add(_animateScale!(scale)); } - if (rotation != null && _animateRotation != null) { - _animateRotation!(rotation); + futures.add(_animateRotation!(rotation)); } + return Future.wait(futures); } @override diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart index 2f775f57e2..66540983a0 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart @@ -237,34 +237,31 @@ class PhotoViewCoreState extends State nextScaleState(); } - void animateScale(double from, double to) { + Future animateScale(double from, double to) { if (!mounted) { - return; + return Future.value(); } _scaleAnimation = Tween(begin: from, end: to).animate(_scaleAnimationController); - _scaleAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _scaleAnimationController.value = 0.0; + return _scaleAnimationController.fling(velocity: 0.4); } - void animatePosition(Offset from, Offset to) { + Future animatePosition(Offset from, Offset to) { if (!mounted) { - return; + return Future.value(); } _positionAnimation = Tween(begin: from, end: to).animate(_positionAnimationController); - _positionAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _positionAnimationController.value = 0.0; + return _positionAnimationController.fling(velocity: 0.4); } - void animateRotation(double from, double to) { + Future animateRotation(double from, double to) { if (!mounted) { - return; + return Future.value(); } _rotationAnimation = Tween(begin: from, end: to).animate(_rotationAnimationController); - _rotationAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _rotationAnimationController.value = 0.0; + return _rotationAnimationController.fling(velocity: 0.4); } void onAnimationStatus(AnimationStatus status) { @@ -280,18 +277,19 @@ class PhotoViewCoreState extends State } } - void _animateControllerPosition(Offset position) { - animatePosition(controller.position, position); + Future _animateControllerPosition(Offset position) { + return animatePosition(controller.position, position); } - void _animateControllerScale(double scale) { + Future _animateControllerScale(double scale) { if (controller.scale != null) { - animateScale(controller.scale!, scale); + return animateScale(controller.scale!, scale); } + return Future.value(); } - void _animateControllerRotation(double rotation) { - animateRotation(controller.rotation, rotation); + Future _animateControllerRotation(double rotation) { + return animateRotation(controller.rotation, rotation); } @override From 5271863291c1c75ededb2eca28b52faf769447d9 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:36:50 +0200 Subject: [PATCH 2/2] fix(mobile): prevent live photo from getting stuck during dismiss animation --- .../asset_viewer/asset_page.widget.dart | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) 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 0934536471..be91dca313 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -48,6 +48,9 @@ class _AssetPageState extends ConsumerState { bool _showingDetails = false; bool _isZoomed = false; + // Frozen during dismiss drag + settle to prevent widget tree swap mid-animation. + bool _frozenMotionPlaying = false; + bool _dismissSettling = false; final _scrollController = SnapScrollController(); double _snapOffset = 0.0; @@ -135,6 +138,9 @@ class _AssetPageState extends ConsumerState { > 0 => _DragIntent.dismiss, _ => _DragIntent.none, }; + if (_dragIntent == _DragIntent.dismiss) { + _frozenMotionPlaying = ref.read(isPlayingMotionVideoProvider); + } } switch (_dragIntent) { @@ -172,12 +178,18 @@ class _AssetPageState extends ConsumerState { context.maybePop(); return; } - _viewController?.animateMultiple( - position: _initialPhotoViewState.position, - scale: _viewController?.initialScale ?? _initialPhotoViewState.scale, - rotation: _initialPhotoViewState.rotation, - ); _viewer.setOpacity(1.0); + _dismissSettling = true; + _viewController + ?.animateMultiple( + position: _initialPhotoViewState.position, + scale: _viewController?.initialScale ?? _initialPhotoViewState.scale, + rotation: _initialPhotoViewState.rotation, + ) + .whenComplete(() { + if (!mounted) return; + setState(() => _dismissSettling = false); + }); } } @@ -355,7 +367,10 @@ class _AssetPageState extends ConsumerState { final currentHeroTag = ref.watch(assetViewerProvider.select((s) => s.currentAsset?.heroTag)); _showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex)); - final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); + final liveMotionPlaying = ref.watch(isPlayingMotionVideoProvider); + final isPlayingMotionVideo = (_dragIntent == _DragIntent.dismiss || _dismissSettling) + ? _frozenMotionPlaying + : liveMotionPlaying; final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index); if (asset == null) {