mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-01 19:29:22 -04:00 
			
		
		
		
	feat(mobile): stop asset grid rebuilds (#3226)
* feat(mobile): stop asset grid rebuilds * undo unnecessary changes --------- Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									863e983726
								
							
						
					
					
						commit
						f9739c9730
					
				| @ -37,12 +37,14 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|   final Asset Function(int index) loadAsset; | ||||
|   final int totalAssets; | ||||
|   final int initialIndex; | ||||
|   final int heroOffset; | ||||
| 
 | ||||
|   GalleryViewerPage({ | ||||
|     super.key, | ||||
|     required this.initialIndex, | ||||
|     required this.loadAsset, | ||||
|     required this.totalAssets, | ||||
|     this.heroOffset = 0, | ||||
|   }) : controller = PageController(initialPage: initialIndex); | ||||
| 
 | ||||
|   final PageController controller; | ||||
| @ -589,7 +591,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|                     }, | ||||
|                     imageProvider: provider, | ||||
|                     heroAttributes: PhotoViewHeroAttributes( | ||||
|                       tag: asset.id, | ||||
|                       tag: asset.id + heroOffset, | ||||
|                     ), | ||||
|                     filterQuality: FilterQuality.high, | ||||
|                     tightMode: true, | ||||
| @ -606,7 +608,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|                     onDragUpdate: (_, details, __) => | ||||
|                         handleSwipeUpDown(details), | ||||
|                     heroAttributes: PhotoViewHeroAttributes( | ||||
|                       tag: asset.id, | ||||
|                       tag: asset.id + heroOffset, | ||||
|                     ), | ||||
|                     filterQuality: FilterQuality.high, | ||||
|                     maxScale: 1.0, | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import 'dart:math'; | ||||
| 
 | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @ -52,84 +53,61 @@ class ImmichAssetGrid extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     var settings = ref.watch(appSettingsServiceProvider); | ||||
| 
 | ||||
|     // Needs to suppress hero animations when navigating to this widget | ||||
|     final enableHeroAnimations = useState(false); | ||||
|     final transitionDuration = ModalRoute.of(context)?.transitionDuration; | ||||
| 
 | ||||
|     final perRow = useState( | ||||
|       assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!, | ||||
|     ); | ||||
|     final scaleFactor = useState(7.0 - perRow.value); | ||||
|     final baseScaleFactor = useState(7.0 - perRow.value); | ||||
| 
 | ||||
|     useEffect( | ||||
|       () { | ||||
|         // Wait for transition to complete, then re-enable | ||||
|         if (transitionDuration == null) { | ||||
|           // No route transition found, maybe we opened this up first | ||||
|           enableHeroAnimations.value = true; | ||||
|         } else { | ||||
|           // Unfortunately, using the transition animation itself didn't | ||||
|           // seem to work reliably. So instead, wait until the duration of the | ||||
|           // animation has elapsed to re-enable the hero animations | ||||
|           Future.delayed(transitionDuration).then((_) { | ||||
|             enableHeroAnimations.value = true; | ||||
|           }); | ||||
|         } | ||||
|         return null; | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
| 
 | ||||
|     Future<bool> onWillPop() async { | ||||
|       enableHeroAnimations.value = false; | ||||
|       return true; | ||||
|     /// assets need different hero tags across tabs / modals | ||||
|     /// otherwise, hero animations are performed across tabs (looks buggy!) | ||||
|     int heroOffset() { | ||||
|       const int range = 1152921504606846976; // 2^60 | ||||
|       final tabScope = TabsRouterScope.of(context); | ||||
|       if (tabScope != null) { | ||||
|         final int tabIndex = tabScope.controller.activeIndex; | ||||
|         return tabIndex * range; | ||||
|       } | ||||
|       return range * 7; | ||||
|     } | ||||
| 
 | ||||
|     Widget buildAssetGridView(RenderList renderList) { | ||||
|       return WillPopScope( | ||||
|         onWillPop: onWillPop, | ||||
|         child: HeroMode( | ||||
|           enabled: enableHeroAnimations.value, | ||||
|           child: RawGestureDetector( | ||||
|             gestures: { | ||||
|               CustomScaleGestureRecognizer: | ||||
|                   GestureRecognizerFactoryWithHandlers< | ||||
|                           CustomScaleGestureRecognizer>( | ||||
|                       () => CustomScaleGestureRecognizer(), | ||||
|                       (CustomScaleGestureRecognizer scale) { | ||||
|                 scale.onStart = (details) { | ||||
|                   baseScaleFactor.value = scaleFactor.value; | ||||
|                 }; | ||||
|       return RawGestureDetector( | ||||
|         gestures: { | ||||
|           CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers< | ||||
|                   CustomScaleGestureRecognizer>( | ||||
|               () => CustomScaleGestureRecognizer(), | ||||
|               (CustomScaleGestureRecognizer scale) { | ||||
|             scale.onStart = (details) { | ||||
|               baseScaleFactor.value = scaleFactor.value; | ||||
|             }; | ||||
| 
 | ||||
|                 scale.onUpdate = (details) { | ||||
|                   scaleFactor.value = | ||||
|                       max(min(5.0, baseScaleFactor.value * details.scale), 1.0); | ||||
|                   if (7 - scaleFactor.value.toInt() != perRow.value) { | ||||
|                     perRow.value = 7 - scaleFactor.value.toInt(); | ||||
|                   } | ||||
|                 }; | ||||
|                 scale.onEnd = (details) {}; | ||||
|               }) | ||||
|             }, | ||||
|             child: ImmichAssetGridView( | ||||
|               onRefresh: onRefresh, | ||||
|               assetsPerRow: perRow.value, | ||||
|               listener: listener, | ||||
|               showStorageIndicator: showStorageIndicator ?? | ||||
|                   settings.getSetting(AppSettingsEnum.storageIndicator), | ||||
|               renderList: renderList, | ||||
|               margin: margin, | ||||
|               selectionActive: selectionActive, | ||||
|               preselectedAssets: preselectedAssets, | ||||
|               canDeselect: canDeselect, | ||||
|               dynamicLayout: dynamicLayout ?? | ||||
|                   settings.getSetting(AppSettingsEnum.dynamicLayout), | ||||
|               showMultiSelectIndicator: showMultiSelectIndicator, | ||||
|               visibleItemsListener: visibleItemsListener, | ||||
|               topWidget: topWidget, | ||||
|             ), | ||||
|           ), | ||||
|             scale.onUpdate = (details) { | ||||
|               scaleFactor.value = | ||||
|                   max(min(5.0, baseScaleFactor.value * details.scale), 1.0); | ||||
|               if (7 - scaleFactor.value.toInt() != perRow.value) { | ||||
|                 perRow.value = 7 - scaleFactor.value.toInt(); | ||||
|               } | ||||
|             }; | ||||
|           }) | ||||
|         }, | ||||
|         child: ImmichAssetGridView( | ||||
|           onRefresh: onRefresh, | ||||
|           assetsPerRow: perRow.value, | ||||
|           listener: listener, | ||||
|           showStorageIndicator: showStorageIndicator ?? | ||||
|               settings.getSetting(AppSettingsEnum.storageIndicator), | ||||
|           renderList: renderList, | ||||
|           margin: margin, | ||||
|           selectionActive: selectionActive, | ||||
|           preselectedAssets: preselectedAssets, | ||||
|           canDeselect: canDeselect, | ||||
|           dynamicLayout: dynamicLayout ?? | ||||
|               settings.getSetting(AppSettingsEnum.dynamicLayout), | ||||
|           showMultiSelectIndicator: showMultiSelectIndicator, | ||||
|           visibleItemsListener: visibleItemsListener, | ||||
|           topWidget: topWidget, | ||||
|           heroOffset: heroOffset(), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @ -34,6 +34,7 @@ class ImmichAssetGridView extends StatefulWidget { | ||||
|   final void Function(ItemPosition start, ItemPosition end)? | ||||
|       visibleItemsListener; | ||||
|   final Widget? topWidget; | ||||
|   final int heroOffset; | ||||
| 
 | ||||
|   const ImmichAssetGridView({ | ||||
|     super.key, | ||||
| @ -50,6 +51,7 @@ class ImmichAssetGridView extends StatefulWidget { | ||||
|     this.showMultiSelectIndicator = true, | ||||
|     this.visibleItemsListener, | ||||
|     this.topWidget, | ||||
|     this.heroOffset = 0, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
| @ -122,6 +124,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> { | ||||
|           : null, | ||||
|       useGrayBoxPlaceholder: true, | ||||
|       showStorageIndicator: widget.showStorageIndicator, | ||||
|       heroOffset: widget.heroOffset, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -18,6 +18,7 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|   final bool multiselectEnabled; | ||||
|   final Function? onSelect; | ||||
|   final Function? onDeselect; | ||||
|   final int heroOffset; | ||||
| 
 | ||||
|   const ThumbnailImage({ | ||||
|     Key? key, | ||||
| @ -31,6 +32,7 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|     this.multiselectEnabled = false, | ||||
|     this.onDeselect, | ||||
|     this.onSelect, | ||||
|     this.heroOffset = 0, | ||||
|   }) : super(key: key); | ||||
| 
 | ||||
|   @override | ||||
| @ -63,6 +65,7 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|               initialIndex: index, | ||||
|               loadAsset: loadAsset, | ||||
|               totalAssets: totalAssets, | ||||
|               heroOffset: heroOffset, | ||||
|             ), | ||||
|           ); | ||||
|         } | ||||
| @ -72,32 +75,7 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|         HapticFeedback.heavyImpact(); | ||||
|       }, | ||||
|       child: Hero( | ||||
|         createRectTween: (begin, end) { | ||||
|           double? top; | ||||
|           // Uses the [BoxFit.contain] algorithm | ||||
|           if (asset.width != null && asset.height != null) { | ||||
|             final assetAR = asset.width! / asset.height!; | ||||
|             final w = MediaQuery.of(context).size.width; | ||||
|             final deviceAR = MediaQuery.of(context).size.aspectRatio; | ||||
|             if (deviceAR < assetAR) { | ||||
|               top = asset.height! * w / asset.width!; | ||||
|             } else { | ||||
|               top = 0; | ||||
|             } | ||||
|             // get the height offset | ||||
|           } | ||||
| 
 | ||||
|           return MaterialRectCenterArcTween( | ||||
|             begin: Rect.fromLTRB( | ||||
|               0, | ||||
|               top ?? 0.0, | ||||
|               MediaQuery.of(context).size.width, | ||||
|               MediaQuery.of(context).size.height, | ||||
|             ), | ||||
|             end: end, | ||||
|           ); | ||||
|         }, | ||||
|         tag: asset.id, | ||||
|         tag: asset.id + heroOffset, | ||||
|         child: Stack( | ||||
|           children: [ | ||||
|             Container( | ||||
|  | ||||
| @ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget { | ||||
|                           onTap: () { | ||||
|                             HapticFeedback.heavyImpact(); | ||||
|                             AutoRouter.of(context).push( | ||||
|                               VerticalRouteView( | ||||
|                               MemoryRoute( | ||||
|                                 memories: memories, | ||||
|                                 memoryIndex: index, | ||||
|                               ), | ||||
|  | ||||
| @ -70,6 +70,7 @@ class _$AppRouter extends RootStackRouter { | ||||
|           initialIndex: args.initialIndex, | ||||
|           loadAsset: args.loadAsset, | ||||
|           totalAssets: args.totalAssets, | ||||
|           heroOffset: args.heroOffset, | ||||
|         ), | ||||
|       ); | ||||
|     }, | ||||
| @ -290,8 +291,8 @@ class _$AppRouter extends RootStackRouter { | ||||
|         child: const AllPeoplePage(), | ||||
|       ); | ||||
|     }, | ||||
|     VerticalRouteView.name: (routeData) { | ||||
|       final args = routeData.argsAs<VerticalRouteViewArgs>(); | ||||
|     MemoryRoute.name: (routeData) { | ||||
|       final args = routeData.argsAs<MemoryRouteArgs>(); | ||||
|       return MaterialPageX<dynamic>( | ||||
|         routeData: routeData, | ||||
|         child: MemoryPage( | ||||
| @ -506,7 +507,7 @@ class _$AppRouter extends RootStackRouter { | ||||
|         ), | ||||
|         RouteConfig( | ||||
|           AlbumViewerRoute.name, | ||||
|           path: '/album-viewer-page', | ||||
|           path: '/', | ||||
|           guards: [ | ||||
|             authGuard, | ||||
|             duplicateGuard, | ||||
| @ -601,8 +602,8 @@ class _$AppRouter extends RootStackRouter { | ||||
|           ], | ||||
|         ), | ||||
|         RouteConfig( | ||||
|           VerticalRouteView.name, | ||||
|           path: '/vertical-page-view', | ||||
|           MemoryRoute.name, | ||||
|           path: '/memory-page', | ||||
|           guards: [ | ||||
|             authGuard, | ||||
|             duplicateGuard, | ||||
| @ -680,6 +681,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> { | ||||
|     required int initialIndex, | ||||
|     required Asset Function(int) loadAsset, | ||||
|     required int totalAssets, | ||||
|     int heroOffset = 0, | ||||
|   }) : super( | ||||
|           GalleryViewerRoute.name, | ||||
|           path: '/gallery-viewer-page', | ||||
| @ -688,6 +690,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> { | ||||
|             initialIndex: initialIndex, | ||||
|             loadAsset: loadAsset, | ||||
|             totalAssets: totalAssets, | ||||
|             heroOffset: heroOffset, | ||||
|           ), | ||||
|         ); | ||||
| 
 | ||||
| @ -700,6 +703,7 @@ class GalleryViewerRouteArgs { | ||||
|     required this.initialIndex, | ||||
|     required this.loadAsset, | ||||
|     required this.totalAssets, | ||||
|     this.heroOffset = 0, | ||||
|   }); | ||||
| 
 | ||||
|   final Key? key; | ||||
| @ -710,9 +714,11 @@ class GalleryViewerRouteArgs { | ||||
| 
 | ||||
|   final int totalAssets; | ||||
| 
 | ||||
|   final int heroOffset; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets}'; | ||||
|     return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset}'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -1014,7 +1020,7 @@ class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> { | ||||
|     required int albumId, | ||||
|   }) : super( | ||||
|           AlbumViewerRoute.name, | ||||
|           path: '/album-viewer-page', | ||||
|           path: '/', | ||||
|           args: AlbumViewerRouteArgs( | ||||
|             key: key, | ||||
|             albumId: albumId, | ||||
| @ -1302,26 +1308,26 @@ class AllPeopleRoute extends PageRouteInfo<void> { | ||||
| 
 | ||||
| /// generated route for | ||||
| /// [MemoryPage] | ||||
| class VerticalRouteView extends PageRouteInfo<VerticalRouteViewArgs> { | ||||
|   VerticalRouteView({ | ||||
| class MemoryRoute extends PageRouteInfo<MemoryRouteArgs> { | ||||
|   MemoryRoute({ | ||||
|     required List<Memory> memories, | ||||
|     required int memoryIndex, | ||||
|     Key? key, | ||||
|   }) : super( | ||||
|           VerticalRouteView.name, | ||||
|           path: '/vertical-page-view', | ||||
|           args: VerticalRouteViewArgs( | ||||
|           MemoryRoute.name, | ||||
|           path: '/memory-page', | ||||
|           args: MemoryRouteArgs( | ||||
|             memories: memories, | ||||
|             memoryIndex: memoryIndex, | ||||
|             key: key, | ||||
|           ), | ||||
|         ); | ||||
| 
 | ||||
|   static const String name = 'VerticalRouteView'; | ||||
|   static const String name = 'MemoryRoute'; | ||||
| } | ||||
| 
 | ||||
| class VerticalRouteViewArgs { | ||||
|   const VerticalRouteViewArgs({ | ||||
| class MemoryRouteArgs { | ||||
|   const MemoryRouteArgs({ | ||||
|     required this.memories, | ||||
|     required this.memoryIndex, | ||||
|     this.key, | ||||
| @ -1335,7 +1341,7 @@ class VerticalRouteViewArgs { | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'VerticalRouteViewArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; | ||||
|     return 'MemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user