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 a61ce0e78b..77fe8634a9 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 701c527613..01970422a8 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 72c4766c45..2f775f57e2 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, ); }