From 39ef77a02a1672c33af531f702dfb1ffdb4dd13d Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:45:23 +0530 Subject: [PATCH] fix: frame skip due to native viewer rebuilds --- .../asset_viewer/asset_page.widget.dart | 18 +++++++++++++++--- .../asset_viewer/asset_viewer.page.dart | 12 +++++++++--- 2 files changed, 24 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 686b3fcf10..5dd0800d90 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -19,10 +19,10 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart' import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; @@ -31,9 +31,16 @@ enum _DragIntent { none, scroll, dismiss } class AssetPage extends ConsumerStatefulWidget { final int index; final int heroOffset; + final Map videoPlayerKeys; final void Function(int direction)? onTapNavigate; - const AssetPage({super.key, required this.index, required this.heroOffset, this.onTapNavigate}); + const AssetPage({ + super.key, + required this.index, + required this.heroOffset, + required this.videoPlayerKeys, + this.onTapNavigate, + }); @override ConsumerState createState() => _AssetPageState(); @@ -293,6 +300,11 @@ class _AssetPageState extends ConsumerState { _listenForScaleBoundaries(controller); } + GlobalKey _getVideoPlayerKey(String id) { + widget.videoPlayerKeys.putIfAbsent(id, () => GlobalKey()); + return widget.videoPlayerKeys[id]!; + } + Widget _buildPhotoView( BaseAsset displayAsset, BaseAsset asset, { @@ -350,7 +362,7 @@ class _AssetPageState extends ConsumerState { enablePanAlways: true, backgroundDecoration: backgroundDecoration, child: NativeVideoViewer( - key: ValueKey(displayAsset), + key: _getVideoPlayerKey(displayAsset.heroTag), asset: displayAsset, scaleStateNotifier: _videoScaleStateNotifier, disableScaleGestures: showingDetails, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index d59e867d66..03c86198b0 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -18,8 +18,8 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widge import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.dart'; 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/viewer_top_app_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; @@ -90,6 +90,7 @@ class _AssetViewerState extends ConsumerState { late final _heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; late final _pageController = PageController(initialPage: widget.initialIndex); late final _preloader = AssetPreloader(timelineService: ref.read(timelineServiceProvider), mounted: () => mounted); + Map _videoPlayerKeys = {}; StreamSubscription? _reloadSubscription; KeepAliveLink? _stackChildrenKeepAlive; @@ -124,6 +125,7 @@ class _AssetViewerState extends ConsumerState { @override void dispose() { + _videoPlayerKeys.clear(); _pageController.dispose(); _preloader.dispose(); _reloadSubscription?.cancel(); @@ -287,8 +289,12 @@ class _AssetViewerState extends ConsumerState { : const FastClampingScrollPhysics(), itemCount: ref.read(timelineServiceProvider).totalAssets, onPageChanged: (index) => _onAssetChanged(index), - itemBuilder: (context, index) => - AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate), + itemBuilder: (context, index) => AssetPage( + index: index, + heroOffset: _heroOffset, + videoPlayerKeys: _videoPlayerKeys, + onTapNavigate: _onTapNavigate, + ), ), ), if (!CurrentPlatform.isIOS)