mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 10:28:50 +01:00
Add hero animation when opening image
This commit is contained in:
parent
fd676e0ac4
commit
a3c98267eb
8 changed files with 97 additions and 51 deletions
21
app/lib/flutter_util.dart
Normal file
21
app/lib/flutter_util.dart
Normal 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})";
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<RemoteImageViewer> {
|
|||
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;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in a new issue