From 05010c3a84e37661de44f0bee158929fb4e6d7a0 Mon Sep 17 00:00:00 2001 From: Thomas <9749173+uhthomas@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:26:53 +0000 Subject: [PATCH] fix(mobile): asset viewer hero animation (#26545) The image in the photo view has no height, and is therefore entirely unconstrained. This causes the image to take up the full height of the viewport during the hero animation, which can make look out of sync. In some other cases, it can stretch or resize the image to fill the entire viewport. --- .../asset_viewer/asset_page.widget.dart | 67 +++++++++---------- .../asset_viewer/video_viewer.widget.dart | 9 +-- .../photo_view/src/core/photo_view_core.dart | 8 ++- 3 files changed, 42 insertions(+), 42 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 a61ce0e78bc1d..77fe8634a911e 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -14,7 +14,6 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details.wi import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; @@ -329,40 +328,40 @@ class _AssetPageState extends ConsumerState { ); } - return Stack( - children: [ - PhotoView.customChild( - key: Key(displayAsset.heroTag), - onDragStart: _onDragStart, - onDragUpdate: _onDragUpdate, - onDragEnd: _onDragEnd, - onDragCancel: _onDragCancel, - onTapUp: _onTapUp, - heroAttributes: heroAttributes, - basePosition: Alignment.center, - disableScaleGestures: showingDetails, - scaleStateChangedCallback: _onScaleStateChanged, - onPageBuild: _onPageBuild, - enablePanAlways: true, - backgroundDecoration: backgroundDecoration, - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewer( - key: _NativeVideoViewerKey(displayAsset.heroTag), - asset: displayAsset, - image: Image( - image: getFullImageProvider(displayAsset, size: context.sizeData), - fit: BoxFit.contain, - height: context.height, - width: context.width, - alignment: Alignment.center, - ), - ), - ), + final Size childSize; + if (displayAsset.width != null && displayAsset.height != null) { + final r = displayAsset.width! / displayAsset.height!; + final w = math.min(context.width, context.height * r); + childSize = Size(w, w / r); + } else { + childSize = Size(context.height, context.height); + } + + return PhotoView.customChild( + key: Key(displayAsset.heroTag), + childSize: childSize, + filterQuality: FilterQuality.low, + onDragStart: _onDragStart, + onDragUpdate: _onDragUpdate, + onDragEnd: _onDragEnd, + onDragCancel: _onDragCancel, + onTapUp: _onTapUp, + heroAttributes: heroAttributes, + basePosition: Alignment.center, + disableScaleGestures: showingDetails, + scaleStateChangedCallback: _onScaleStateChanged, + onPageBuild: _onPageBuild, + enablePanAlways: true, + backgroundDecoration: backgroundDecoration, + child: NativeVideoViewer( + key: _NativeVideoViewerKey(displayAsset.heroTag), + asset: displayAsset, + image: Image( + image: getFullImageProvider(displayAsset, size: childSize), + fit: BoxFit.contain, + alignment: Alignment.center, ), - const Center(child: VideoViewerControls()), - ], + ), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 701c52761300c..01970422a8134 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/domain/services/setting.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; @@ -393,13 +394,9 @@ class NativeVideoViewer extends HookConsumerWidget { if (aspectRatio.value != null && !isCasting) Visibility.maintain( visible: isVisible.value, - child: Center( - child: AspectRatio( - aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(onViewReady: initController) : null, - ), - ), + child: NativeVideoPlayerView(onViewReady: initController), ), + const Center(child: VideoViewerControls()), ], ); } 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 72c4766c45f69..2f775f57e2d15 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 @@ -420,7 +420,11 @@ class PhotoViewCoreState extends State Widget _buildChild() { return widget.hasCustomChild - ? widget.customChild! + ? SizedBox( + width: scaleBoundaries.childSize.width * scale, + height: scaleBoundaries.childSize.height * scale, + child: widget.customChild!, + ) : Image( key: widget.heroAttributes?.tag != null ? ObjectKey(widget.heroAttributes!.tag) : null, image: widget.imageProvider!, @@ -428,7 +432,7 @@ class PhotoViewCoreState extends State gaplessPlayback: widget.gaplessPlayback ?? false, filterQuality: widget.filterQuality, width: scaleBoundaries.childSize.width * scale, - fit: BoxFit.cover, + fit: BoxFit.contain, isAntiAlias: widget.filterQuality == FilterQuality.high, ); }