From a3c98267eb883e839d2e381d56aa289fac60ecc9 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 7 Sep 2022 17:37:50 +0800 Subject: [PATCH] Add hero animation when opening image --- app/lib/flutter_util.dart | 21 +++++++ app/lib/k.dart | 4 +- app/lib/widget/album_browser.dart | 2 + app/lib/widget/dynamic_album_browser.dart | 2 + app/lib/widget/image_viewer.dart | 38 ++++++------ app/lib/widget/photo_list_item.dart | 71 +++++++++++++---------- app/lib/widget/smart_album_browser.dart | 2 + app/lib/widget/viewer.dart | 8 ++- 8 files changed, 97 insertions(+), 51 deletions(-) create mode 100644 app/lib/flutter_util.dart diff --git a/app/lib/flutter_util.dart b/app/lib/flutter_util.dart new file mode 100644 index 00000000..273f0e16 --- /dev/null +++ b/app/lib/flutter_util.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:nc_photos/entity/file.dart'; + +class CustomizableMaterialPageRoute extends MaterialPageRoute { + CustomizableMaterialPageRoute({ + required super.builder, + super.settings, + super.maintainState, + super.fullscreenDialog, + required this.transitionDuration, + required this.reverseTransitionDuration, + }); + + @override + final Duration transitionDuration; + + @override + final Duration reverseTransitionDuration; +} + +String getImageHeroTag(File file) => "imageHero(${file.path})"; diff --git a/app/lib/k.dart b/app/lib/k.dart index b2e4893e..27955496 100644 --- a/app/lib/k.dart +++ b/app/lib/k.dart @@ -18,7 +18,9 @@ const animationDurationNormal = Duration(milliseconds: 250); const animationDurationLong = Duration(milliseconds: 500); /// Duration for tab transition animation -const animationDurationTabTransition = Duration(milliseconds: 400); +const animationDurationTabTransition = Duration(milliseconds: 350); + +const heroDurationNormal = Duration(milliseconds: 450); /// Size of the photo/video thumbnails /// diff --git a/app/lib/widget/album_browser.dart b/app/lib/widget/album_browser.dart index 4f1a6a1c..eb905a54 100644 --- a/app/lib/widget/album_browser.dart +++ b/app/lib/widget/album_browser.dart @@ -17,6 +17,7 @@ import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/list_extension.dart'; import 'package:nc_photos/object_extension.dart'; @@ -974,6 +975,7 @@ class _ImageListItem extends _FileListItem { account: account, previewUrl: previewUrl, isGif: file.contentType == "image/gif", + heroKey: flutter_util.getImageHeroTag(file), ); final Account account; diff --git a/app/lib/widget/dynamic_album_browser.dart b/app/lib/widget/dynamic_album_browser.dart index fc44d892..7fc047fb 100644 --- a/app/lib/widget/dynamic_album_browser.dart +++ b/app/lib/widget/dynamic_album_browser.dart @@ -18,6 +18,7 @@ import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/or_null.dart'; @@ -716,6 +717,7 @@ class _ImageListItem extends _FileListItem { account: account, previewUrl: previewUrl, isGif: file.contentType == "image/gif", + heroKey: flutter_util.getImageHeroTag(file), ); final Account account; diff --git a/app/lib/widget/image_viewer.dart b/app/lib/widget/image_viewer.dart index a1b3632c..2b01982e 100644 --- a/app/lib/widget/image_viewer.dart +++ b/app/lib/widget/image_viewer.dart @@ -8,6 +8,7 @@ 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.dart' as app; 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; @@ -115,23 +116,26 @@ class _RemoteImageViewerState extends State { 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; - }, + child: Hero( + tag: flutter_util.getImageHeroTag(widget.file), + 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; + }, + ), ), ); diff --git a/app/lib/widget/photo_list_item.dart b/app/lib/widget/photo_list_item.dart index adb6c772..7e8b1ef2 100644 --- a/app/lib/widget/photo_list_item.dart +++ b/app/lib/widget/photo_list_item.dart @@ -7,6 +7,7 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/cache_manager_util.dart'; +import 'package:nc_photos/flutter_util.dart'as flutter_util; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/k.dart' as k; @@ -65,6 +66,7 @@ class PhotoListImageItem extends PhotoListFileItem { previewUrl: previewUrl, isGif: file.contentType == "image/gif", isFavorite: shouldShowFavoriteBadge && file.isFavorite == true, + heroKey: flutter_util.getImageHeroTag(file), ); final Account account; @@ -214,10 +216,45 @@ class PhotoListImage extends StatelessWidget { this.padding = const EdgeInsets.all(2), this.isGif = false, this.isFavorite = false, + this.heroKey, }) : super(key: key); @override build(BuildContext context) { + Widget buildPlaceholder() => Center( + child: Icon( + Icons.image_not_supported, + size: 64, + color: Colors.white.withOpacity(.8), + ), + ); + Widget child; + if (previewUrl == null) { + child = buildPlaceholder(); + } else { + child = CachedNetworkImage( + cacheManager: ThumbnailCacheManager.inst, + imageUrl: previewUrl!, + httpHeaders: { + "Authorization": Api.getAuthorizationHeaderValue(account), + }, + fadeInDuration: const Duration(), + filterQuality: FilterQuality.high, + errorWidget: (context, url, error) { + // won't work on web because the image is downloaded by + // the cache manager instead + return buildPlaceholder(); + }, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + ); + if (heroKey != null) { + child = Hero( + tag: heroKey!, + child: child, + ); + } + } + return Padding( padding: padding, child: FittedBox( @@ -229,37 +266,7 @@ class PhotoListImage extends StatelessWidget { // arbitrary size here constraints: BoxConstraints.tight(const Size(128, 128)), color: AppTheme.getListItemBackgroundColor(context), - child: previewUrl == null - ? Center( - child: Icon( - Icons.image_not_supported, - size: 64, - color: Colors.white.withOpacity(.8), - ), - ) - : CachedNetworkImage( - cacheManager: ThumbnailCacheManager.inst, - imageUrl: previewUrl!, - httpHeaders: { - "Authorization": - Api.getAuthorizationHeaderValue(account), - }, - fadeInDuration: const Duration(), - filterQuality: FilterQuality.high, - errorWidget: (context, url, error) { - // won't work on web because the image is downloaded by - // the cache manager instead - // where's the preview??? - return Center( - child: Icon( - Icons.image_not_supported, - size: 64, - color: Colors.white.withOpacity(.8), - ), - ); - }, - imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, - ), + child: child, ), if (isGif) Container( @@ -296,6 +303,8 @@ class PhotoListImage extends StatelessWidget { final bool isGif; final EdgeInsetsGeometry padding; final bool isFavorite; + // if not null, the image will be contained by a Hero widget + final String? heroKey; } class PhotoListVideo extends StatelessWidget { diff --git a/app/lib/widget/smart_album_browser.dart b/app/lib/widget/smart_album_browser.dart index 57e4375d..f9a9b17a 100644 --- a/app/lib/widget/smart_album_browser.dart +++ b/app/lib/widget/smart_album_browser.dart @@ -12,6 +12,7 @@ import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/share_handler.dart'; @@ -385,6 +386,7 @@ class _ImageListItem extends _FileListItem { account: account, previewUrl: previewUrl, isGif: file.contentType == "image/gif", + heroKey: flutter_util.getImageHeroTag(file), ); final Account account; diff --git a/app/lib/widget/viewer.dart b/app/lib/widget/viewer.dart index 621160fb..5756692e 100644 --- a/app/lib/widget/viewer.dart +++ b/app/lib/widget/viewer.dart @@ -14,6 +14,7 @@ import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/flutter_util.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/platform/features.dart' as features; @@ -52,8 +53,11 @@ class ViewerArguments { class Viewer extends StatefulWidget { static const routeName = "/viewer"; - static Route buildRoute(ViewerArguments args) => MaterialPageRoute( - builder: (context) => Viewer.fromArgs(args), + static Route buildRoute(ViewerArguments args) => + CustomizableMaterialPageRoute( + transitionDuration: k.heroDurationNormal, + reverseTransitionDuration: k.heroDurationNormal, + builder: (_) => Viewer.fromArgs(args), ); const Viewer({