Add hero animation when opening image

This commit is contained in:
Ming Ming 2022-09-07 17:37:50 +08:00
parent fd676e0ac4
commit a3c98267eb
8 changed files with 97 additions and 51 deletions

21
app/lib/flutter_util.dart Normal file
View file

@ -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})";

View file

@ -18,7 +18,9 @@ const animationDurationNormal = Duration(milliseconds: 250);
const animationDurationLong = Duration(milliseconds: 500); const animationDurationLong = Duration(milliseconds: 500);
/// Duration for tab transition animation /// 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 /// Size of the photo/video thumbnails
/// ///

View file

@ -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/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; 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/k.dart' as k;
import 'package:nc_photos/list_extension.dart'; import 'package:nc_photos/list_extension.dart';
import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/object_extension.dart';
@ -974,6 +975,7 @@ class _ImageListItem extends _FileListItem {
account: account, account: account,
previewUrl: previewUrl, previewUrl: previewUrl,
isGif: file.contentType == "image/gif", isGif: file.contentType == "image/gif",
heroKey: flutter_util.getImageHeroTag(file),
); );
final Account account; final Account account;

View file

@ -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/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; 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/k.dart' as k;
import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/or_null.dart';
@ -716,6 +717,7 @@ class _ImageListItem extends _FileListItem {
account: account, account: account,
previewUrl: previewUrl, previewUrl: previewUrl,
isGif: file.contentType == "image/gif", isGif: file.contentType == "image/gif",
heroKey: flutter_util.getImageHeroTag(file),
); );
final Account account; final Account account;

View file

@ -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/cache_manager_util.dart';
import 'package:nc_photos/entity/file.dart' as app; import 'package:nc_photos/entity/file.dart' as app;
import 'package:nc_photos/entity/local_file.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/k.dart' as k;
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart'; 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/cached_network_image_mod.dart' as mod;
@ -115,23 +116,26 @@ class _RemoteImageViewerState extends State<RemoteImageViewer> {
onHeightChanged: widget.onHeightChanged, onHeightChanged: widget.onHeightChanged,
onZoomStarted: widget.onZoomStarted, onZoomStarted: widget.onZoomStarted,
onZoomEnded: widget.onZoomEnded, onZoomEnded: widget.onZoomEnded,
child: mod.CachedNetworkImage( child: Hero(
cacheManager: LargeImageCacheManager.inst, tag: flutter_util.getImageHeroTag(widget.file),
imageUrl: _getImageUrl(widget.account, widget.file), child: mod.CachedNetworkImage(
httpHeaders: { cacheManager: LargeImageCacheManager.inst,
"Authorization": Api.getAuthorizationHeaderValue(widget.account), imageUrl: _getImageUrl(widget.account, widget.file),
}, httpHeaders: {
fit: BoxFit.contain, "Authorization": Api.getAuthorizationHeaderValue(widget.account),
fadeInDuration: const Duration(), },
filterQuality: FilterQuality.high, fit: BoxFit.contain,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, fadeInDuration: const Duration(),
imageBuilder: (context, child, imageProvider) { filterQuality: FilterQuality.high,
WidgetsBinding.instance.addPostFrameCallback((_) { imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
_onItemLoaded(); imageBuilder: (context, child, imageProvider) {
}); WidgetsBinding.instance.addPostFrameCallback((_) {
const SizeChangedLayoutNotification().dispatch(context); _onItemLoaded();
return child; });
}, const SizeChangedLayoutNotification().dispatch(context);
return child;
},
),
), ),
); );

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.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/file.dart';
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -65,6 +66,7 @@ class PhotoListImageItem extends PhotoListFileItem {
previewUrl: previewUrl, previewUrl: previewUrl,
isGif: file.contentType == "image/gif", isGif: file.contentType == "image/gif",
isFavorite: shouldShowFavoriteBadge && file.isFavorite == true, isFavorite: shouldShowFavoriteBadge && file.isFavorite == true,
heroKey: flutter_util.getImageHeroTag(file),
); );
final Account account; final Account account;
@ -214,10 +216,45 @@ class PhotoListImage extends StatelessWidget {
this.padding = const EdgeInsets.all(2), this.padding = const EdgeInsets.all(2),
this.isGif = false, this.isGif = false,
this.isFavorite = false, this.isFavorite = false,
this.heroKey,
}) : super(key: key); }) : super(key: key);
@override @override
build(BuildContext context) { 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( return Padding(
padding: padding, padding: padding,
child: FittedBox( child: FittedBox(
@ -229,37 +266,7 @@ class PhotoListImage extends StatelessWidget {
// arbitrary size here // arbitrary size here
constraints: BoxConstraints.tight(const Size(128, 128)), constraints: BoxConstraints.tight(const Size(128, 128)),
color: AppTheme.getListItemBackgroundColor(context), color: AppTheme.getListItemBackgroundColor(context),
child: previewUrl == null child: child,
? 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,
),
), ),
if (isGif) if (isGif)
Container( Container(
@ -296,6 +303,8 @@ class PhotoListImage extends StatelessWidget {
final bool isGif; final bool isGif;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final bool isFavorite; final bool isFavorite;
// if not null, the image will be contained by a Hero widget
final String? heroKey;
} }
class PhotoListVideo extends StatelessWidget { class PhotoListVideo extends StatelessWidget {

View file

@ -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/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; 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/k.dart' as k;
import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/share_handler.dart';
@ -385,6 +386,7 @@ class _ImageListItem extends _FileListItem {
account: account, account: account,
previewUrl: previewUrl, previewUrl: previewUrl,
isGif: file.contentType == "image/gif", isGif: file.contentType == "image/gif",
heroKey: flutter_util.getImageHeroTag(file),
); );
final Account account; final Account account;

View file

@ -14,6 +14,7 @@ import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; 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/k.dart' as k;
import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/notified_action.dart';
import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/platform/features.dart' as features;
@ -52,8 +53,11 @@ class ViewerArguments {
class Viewer extends StatefulWidget { class Viewer extends StatefulWidget {
static const routeName = "/viewer"; static const routeName = "/viewer";
static Route buildRoute(ViewerArguments args) => MaterialPageRoute( static Route buildRoute(ViewerArguments args) =>
builder: (context) => Viewer.fromArgs(args), CustomizableMaterialPageRoute(
transitionDuration: k.heroDurationNormal,
reverseTransitionDuration: k.heroDurationNormal,
builder: (_) => Viewer.fromArgs(args),
); );
const Viewer({ const Viewer({