diff --git a/app/lib/flutter_util.dart b/app/lib/flutter_util.dart index 6f7d4cf1..367c9b04 100644 --- a/app/lib/flutter_util.dart +++ b/app/lib/flutter_util.dart @@ -19,3 +19,44 @@ class CustomizableMaterialPageRoute extends MaterialPageRoute { } String getImageHeroTag(FileDescriptor file) => "imageHero(${file.fdPath})"; + +// copied from flutter +Widget defaultHeroFlightShuttleBuilder( + BuildContext flightContext, + Animation animation, + HeroFlightDirection flightDirection, + BuildContext fromHeroContext, + BuildContext toHeroContext, +) { + final Hero toHero = toHeroContext.widget as Hero; + + final MediaQueryData? toMediaQueryData = MediaQuery.maybeOf(toHeroContext); + final MediaQueryData? fromMediaQueryData = + MediaQuery.maybeOf(fromHeroContext); + + if (toMediaQueryData == null || fromMediaQueryData == null) { + return toHero.child; + } + + final EdgeInsets fromHeroPadding = fromMediaQueryData.padding; + final EdgeInsets toHeroPadding = toMediaQueryData.padding; + + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: toMediaQueryData.copyWith( + padding: (flightDirection == HeroFlightDirection.push) + ? EdgeInsetsTween( + begin: fromHeroPadding, + end: toHeroPadding, + ).evaluate(animation) + : EdgeInsetsTween( + begin: toHeroPadding, + end: fromHeroPadding, + ).evaluate(animation), + ), + child: toHero.child); + }, + ); +} diff --git a/app/lib/widget/image_viewer.dart b/app/lib/widget/image_viewer.dart index 43479b33..741cbf66 100644 --- a/app/lib/widget/image_viewer.dart +++ b/app/lib/widget/image_viewer.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; @@ -8,9 +9,11 @@ import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/local_file.dart'; +import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/content_uri_image_provider.dart'; import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod; +import 'package:nc_photos/widget/network_thumbnail.dart'; import 'package:np_codegen/np_codegen.dart'; part 'image_viewer.g.dart'; @@ -113,30 +116,78 @@ class RemoteImageViewer extends StatefulWidget { @npLog class _RemoteImageViewerState extends State { @override - build(BuildContext context) => _ImageViewer( - canZoom: widget.canZoom, - onHeightChanged: widget.onHeightChanged, - onZoomStarted: widget.onZoomStarted, - onZoomEnded: widget.onZoomEnded, - child: mod.CachedNetworkImage( - cacheManager: LargeImageCacheManager.inst, - imageUrl: _getImageUrl(widget.account, widget.file), - httpHeaders: { - "Authorization": Api.getAuthorizationHeaderValue(widget.account), - }, - fit: BoxFit.contain, - fadeInDuration: const Duration(), - filterQuality: FilterQuality.high, - imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, - imageBuilder: (context, child, imageProvider) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _onItemLoaded(); - }); - const SizeChangedLayoutNotification().dispatch(context); - return child; - }, - ), - ); + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + // needed to get rid of the large image blinking during Hero animation + setState(() {}); + } + }); + } + + @override + Widget build(BuildContext context) { + return _ImageViewer( + canZoom: widget.canZoom, + onHeightChanged: widget.onHeightChanged, + onZoomStarted: widget.onZoomStarted, + onZoomEnded: widget.onZoomEnded, + child: Stack( + fit: StackFit.passthrough, + children: [ + Hero( + tag: flutter_util.getImageHeroTag(widget.file), + flightShuttleBuilder: (flightContext, animation, flightDirection, + fromHeroContext, toHeroContext) { + _isHeroDone = false; + animation.addStatusListener(_animationListener); + return flutter_util.defaultHeroFlightShuttleBuilder( + flightContext, + animation, + flightDirection, + fromHeroContext, + toHeroContext, + ); + }, + child: CachedNetworkImage( + fit: BoxFit.contain, + cacheManager: ThumbnailCacheManager.inst, + imageUrl: NetworkRectThumbnail.imageUrlForFile( + widget.account, widget.file), + httpHeaders: { + "Authorization": + Api.getAuthorizationHeaderValue(widget.account), + }, + fadeInDuration: const Duration(), + filterQuality: FilterQuality.high, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + ), + ), + if (_isHeroDone) + mod.CachedNetworkImage( + cacheManager: LargeImageCacheManager.inst, + imageUrl: _getImageUrl(widget.account, widget.file), + httpHeaders: { + "Authorization": + Api.getAuthorizationHeaderValue(widget.account), + }, + fit: BoxFit.contain, + fadeInDuration: const Duration(), + filterQuality: FilterQuality.high, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + imageBuilder: (context, child, imageProvider) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _onItemLoaded(); + }); + const SizeChangedLayoutNotification().dispatch(context); + return child; + }, + ), + ], + ), + ); + } void _onItemLoaded() { if (!_isLoaded) { @@ -146,7 +197,19 @@ class _RemoteImageViewerState extends State { } } + void _animationListener(AnimationStatus status) { + if (status == AnimationStatus.completed) { + _isHeroDone = true; + if (mounted) { + setState(() {}); + } + } + } + var _isLoaded = false; + // initially set to true such that the large image will show when hero didn't + // run (i.e., when swiping in viewer) + var _isHeroDone = true; } class _ImageViewer extends StatefulWidget { diff --git a/app/lib/widget/photo_list_item.dart b/app/lib/widget/photo_list_item.dart index 5ce05a23..1b25a5fb 100644 --- a/app/lib/widget/photo_list_item.dart +++ b/app/lib/widget/photo_list_item.dart @@ -6,6 +6,7 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/local_file.dart'; +import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/content_uri_image_provider.dart'; import 'package:nc_photos/theme.dart'; @@ -63,6 +64,7 @@ class PhotoListImageItem extends PhotoListFileItem { previewUrl: previewUrl, isGif: file.fdMime == "image/gif", isFavorite: shouldShowFavoriteBadge && file.fdIsFavorite == true, + heroKey: flutter_util.getImageHeroTag(file), ); final Account account;