Compare commits

...

4 Commits

Author SHA1 Message Date
Luis Nachtigall ed06dc40d1 Merge branch 'main' into bugfix/live-photo-stuck 2026-05-07 14:46:33 +02:00
Luis Nachtigall 14193b82df Merge branch 'main' into bugfix/live-photo-stuck 2026-04-27 11:07:22 +02:00
LeLunZ 5271863291 fix(mobile): prevent live photo from getting stuck during dismiss animation 2026-04-25 16:36:50 +02:00
LeLunZ 6b3e07dc52 update animation methods to return futures 2026-04-25 13:02:01 +02:00
3 changed files with 58 additions and 44 deletions
@@ -49,6 +49,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
bool _showingDetails = false; bool _showingDetails = false;
bool _isZoomed = 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(); final _scrollController = SnapScrollController();
double _snapOffset = 0.0; double _snapOffset = 0.0;
@@ -136,6 +139,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
> 0 => _DragIntent.dismiss, > 0 => _DragIntent.dismiss,
_ => _DragIntent.none, _ => _DragIntent.none,
}; };
if (_dragIntent == _DragIntent.dismiss) {
_frozenMotionPlaying = ref.read(isPlayingMotionVideoProvider);
}
} }
switch (_dragIntent) { switch (_dragIntent) {
@@ -173,12 +179,18 @@ class _AssetPageState extends ConsumerState<AssetPage> {
context.maybePop(); context.maybePop();
return; return;
} }
_viewController?.animateMultiple(
position: _initialPhotoViewState.position,
scale: _viewController?.initialScale ?? _initialPhotoViewState.scale,
rotation: _initialPhotoViewState.rotation,
);
_viewer.setOpacity(1.0); _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);
});
} }
} }
@@ -356,7 +368,10 @@ class _AssetPageState extends ConsumerState<AssetPage> {
final currentHeroTag = ref.watch(assetViewerProvider.select((s) => s.currentAsset?.heroTag)); final currentHeroTag = ref.watch(assetViewerProvider.select((s) => s.currentAsset?.heroTag));
_showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); _showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex)); 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); final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index);
if (asset == null) { if (asset == null) {
@@ -38,12 +38,13 @@ abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
/// Closes streams and removes eventual listeners. /// Closes streams and removes eventual listeners.
void dispose(); void dispose();
void positionAnimationBuilder(void Function(Offset)? value); void positionAnimationBuilder(Future<void> Function(Offset)? value);
void scaleAnimationBuilder(void Function(double)? value); void scaleAnimationBuilder(Future<void> Function(double)? value);
void rotationAnimationBuilder(void Function(double)? value); void rotationAnimationBuilder(Future<void> Function(double)? value);
/// Animates multiple fields of the state /// Animates multiple fields of the state. The returned future completes
void animateMultiple({Offset? position, double? scale, double? rotation}); /// when all underlying animations have settled.
Future<void> animateMultiple({Offset? position, double? scale, double? rotation});
/// Add a listener that will ignore updates made internally /// Add a listener that will ignore updates made internally
/// ///
@@ -148,9 +149,9 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
@override @override
ScaleBoundaries? scaleBoundaries; ScaleBoundaries? scaleBoundaries;
late void Function(Offset)? _animatePosition; late Future<void> Function(Offset)? _animatePosition;
late void Function(double)? _animateScale; late Future<void> Function(double)? _animateScale;
late void Function(double)? _animateRotation; late Future<void> Function(double)? _animateRotation;
@override @override
Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream; Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream;
@@ -159,17 +160,17 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
late PhotoViewControllerValue prevValue; late PhotoViewControllerValue prevValue;
@override @override
void positionAnimationBuilder(void Function(Offset)? value) { void positionAnimationBuilder(Future<void> Function(Offset)? value) {
_animatePosition = value; _animatePosition = value;
} }
@override @override
void scaleAnimationBuilder(void Function(double)? value) { void scaleAnimationBuilder(Future<void> Function(double)? value) {
_animateScale = value; _animateScale = value;
} }
@override @override
void rotationAnimationBuilder(void Function(double)? value) { void rotationAnimationBuilder(Future<void> Function(double)? value) {
_animateRotation = value; _animateRotation = value;
} }
@@ -193,18 +194,18 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
} }
@override @override
void animateMultiple({Offset? position, double? scale, double? rotation}) { Future<void> animateMultiple({Offset? position, double? scale, double? rotation}) {
final futures = <Future<void>>[];
if (position != null && _animatePosition != null) { if (position != null && _animatePosition != null) {
_animatePosition!(position); futures.add(_animatePosition!(position));
} }
if (scale != null && _animateScale != null) { if (scale != null && _animateScale != null) {
_animateScale!(scale); futures.add(_animateScale!(scale));
} }
if (rotation != null && _animateRotation != null) { if (rotation != null && _animateRotation != null) {
_animateRotation!(rotation); futures.add(_animateRotation!(rotation));
} }
return Future.wait(futures);
} }
@override @override
@@ -237,34 +237,31 @@ class PhotoViewCoreState extends State<PhotoViewCore>
nextScaleState(); nextScaleState();
} }
void animateScale(double from, double to) { Future<void> animateScale(double from, double to) {
if (!mounted) { if (!mounted) {
return; return Future.value();
} }
_scaleAnimation = Tween<double>(begin: from, end: to).animate(_scaleAnimationController); _scaleAnimation = Tween<double>(begin: from, end: to).animate(_scaleAnimationController);
_scaleAnimationController _scaleAnimationController.value = 0.0;
..value = 0.0 return _scaleAnimationController.fling(velocity: 0.4);
..fling(velocity: 0.4);
} }
void animatePosition(Offset from, Offset to) { Future<void> animatePosition(Offset from, Offset to) {
if (!mounted) { if (!mounted) {
return; return Future.value();
} }
_positionAnimation = Tween<Offset>(begin: from, end: to).animate(_positionAnimationController); _positionAnimation = Tween<Offset>(begin: from, end: to).animate(_positionAnimationController);
_positionAnimationController _positionAnimationController.value = 0.0;
..value = 0.0 return _positionAnimationController.fling(velocity: 0.4);
..fling(velocity: 0.4);
} }
void animateRotation(double from, double to) { Future<void> animateRotation(double from, double to) {
if (!mounted) { if (!mounted) {
return; return Future.value();
} }
_rotationAnimation = Tween<double>(begin: from, end: to).animate(_rotationAnimationController); _rotationAnimation = Tween<double>(begin: from, end: to).animate(_rotationAnimationController);
_rotationAnimationController _rotationAnimationController.value = 0.0;
..value = 0.0 return _rotationAnimationController.fling(velocity: 0.4);
..fling(velocity: 0.4);
} }
void onAnimationStatus(AnimationStatus status) { void onAnimationStatus(AnimationStatus status) {
@@ -280,18 +277,19 @@ class PhotoViewCoreState extends State<PhotoViewCore>
} }
} }
void _animateControllerPosition(Offset position) { Future<void> _animateControllerPosition(Offset position) {
animatePosition(controller.position, position); return animatePosition(controller.position, position);
} }
void _animateControllerScale(double scale) { Future<void> _animateControllerScale(double scale) {
if (controller.scale != null) { if (controller.scale != null) {
animateScale(controller.scale!, scale); return animateScale(controller.scale!, scale);
} }
return Future.value();
} }
void _animateControllerRotation(double rotation) { Future<void> _animateControllerRotation(double rotation) {
animateRotation(controller.rotation, rotation); return animateRotation(controller.rotation, rotation);
} }
@override @override