From f4e5018b788e006f333335568122c53b39f5b8d1 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 16 Sep 2024 00:07:14 +0200 Subject: [PATCH] #1174 custom placeholder handling for collection-viewer hero --- lib/widgets/aves_app.dart | 2 ++ lib/widgets/collection/collection_grid.dart | 23 +++++++++++++++++-- lib/widgets/collection/grid/tile.dart | 16 +++++++++++++ .../providers/viewer_entry_provider.dart | 17 ++++++++++++++ lib/widgets/common/thumbnail/decorated.dart | 7 +++++- lib/widgets/common/thumbnail/image.dart | 3 +++ lib/widgets/viewer/entry_viewer_stack.dart | 2 ++ 7 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 lib/widgets/common/providers/viewer_entry_provider.dart diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 0d3d44607..4af3dbb6d 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -32,6 +32,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/durations_provider.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/providers/viewer_entry_provider.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/navigation/tv_page_transitions.dart'; import 'package:aves/widgets/navigation/tv_rail.dart'; @@ -224,6 +225,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { Provider.value(value: _tvRailController), DurationsProvider(), HighlightInfoProvider(), + ViewerEntryProvider(), ], child: NotificationListener( onNotification: (notification) { diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 76b14adbc..9f5be53ee 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -40,6 +40,7 @@ import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; +import 'package:aves/widgets/common/providers/viewer_entry_provider.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/notifications.dart'; @@ -49,6 +50,7 @@ import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -116,6 +118,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { final ValueNotifier _isScrollingNotifier = ValueNotifier(false); final ValueNotifier _selectingAppModeNotifier = ValueNotifier(AppMode.pickFilteredMediaInternal); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => context.read().value = null); + } + @override void dispose() { _focusedItemNotifier.dispose(); @@ -238,9 +246,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { ); } - void _goToViewer(CollectionLens collection, AvesEntry entry) { + Future _goToViewer(CollectionLens collection, AvesEntry entry) async { + // track viewer entry for dynamic hero placeholder + WidgetsBinding.instance.addPostFrameCallback((_) => context.read().value = entry); + final selection = context.read>(); - Navigator.maybeOf(context)?.push( + await Navigator.maybeOf(context)?.push( TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (context, a, sa) { @@ -266,6 +277,14 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { }, ), ); + + // reset track viewer entry + final animate = context.read().animate; + if (animate) { + // TODO TLAD fix timing when transition is incomplete, e.g. when going back while going to the viewer + await Future.delayed(ADurations.pageTransitionExact * timeDilation); + } + context.read().value = null; } } diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index d6b31b519..c9400c784 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -6,6 +6,7 @@ import 'package:aves/services/intent_service.dart'; import 'package:aves/widgets/collection/grid/list_details.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/common/grid/scaling.dart'; +import 'package:aves/widgets/common/providers/viewer_entry_provider.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/notifications.dart'; import 'package:aves/widgets/viewer/hero.dart'; @@ -124,5 +125,20 @@ class Tile extends StatelessWidget { selectable: selectable, highlightable: highlightable, heroTagger: heroTagger, + // do not use a hero placeholder but hide the thumbnail matching the viewer entry, + // so that it can hero out on an entry and come back with a hero to a different entry + heroPlaceholderBuilder: (context, heroSize, child) => child, + imageDecorator: (context, child) { + return Selector( + selector: (context, v) => v.value == entry, + builder: (context, isViewerEntry, child) { + return Visibility.maintain( + visible: !isViewerEntry, + child: child!, + ); + }, + child: child, + ); + }, ); } diff --git a/lib/widgets/common/providers/viewer_entry_provider.dart b/lib/widgets/common/providers/viewer_entry_provider.dart new file mode 100644 index 000000000..ca3ee3428 --- /dev/null +++ b/lib/widgets/common/providers/viewer_entry_provider.dart @@ -0,0 +1,17 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class ViewerEntryProvider extends ListenableProvider { + ViewerEntryProvider({ + super.key, + super.child, + }) : super( + create: (context) => ViewerEntryNotifier(null), + dispose: (context, value) => value.dispose(), + ); +} + +class ViewerEntryNotifier extends ValueNotifier { + ViewerEntryNotifier(super.value); +} diff --git a/lib/widgets/common/thumbnail/decorated.dart b/lib/widgets/common/thumbnail/decorated.dart index 34ddd3709..f4b4190e8 100644 --- a/lib/widgets/common/thumbnail/decorated.dart +++ b/lib/widgets/common/thumbnail/decorated.dart @@ -13,6 +13,8 @@ class DecoratedThumbnail extends StatelessWidget { final ValueNotifier? cancellableNotifier; final bool isMosaic, selectable, highlightable; final Object? Function()? heroTagger; + final HeroPlaceholderBuilder? heroPlaceholderBuilder; + final TransitionBuilder? imageDecorator; static Color borderColor(BuildContext context) => Theme.of(context).dividerColor; @@ -27,6 +29,8 @@ class DecoratedThumbnail extends StatelessWidget { this.selectable = true, this.highlightable = true, this.heroTagger, + this.heroPlaceholderBuilder, + this.imageDecorator, }); @override @@ -50,12 +54,13 @@ class DecoratedThumbnail extends StatelessWidget { isMosaic: isMosaic, cancellableNotifier: cancellableNotifier, heroTag: heroTagger?.call(), + heroPlaceholderBuilder: heroPlaceholderBuilder, ); child = Stack( fit: StackFit.passthrough, children: [ - child, + imageDecorator?.call(context, child) ?? child, ThumbnailEntryOverlay(entry: entry), if (selectable) ...[ GridItemSelectionOverlay( diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 0760c109e..87154a755 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -25,6 +25,7 @@ class ThumbnailImage extends StatefulWidget { final bool showLoadingBackground; final ValueNotifier? cancellableNotifier; final Object? heroTag; + final HeroPlaceholderBuilder? heroPlaceholderBuilder; const ThumbnailImage({ super.key, @@ -37,6 +38,7 @@ class ThumbnailImage extends StatefulWidget { this.showLoadingBackground = true, this.cancellableNotifier, this.heroTag, + this.heroPlaceholderBuilder, }); @override @@ -283,6 +285,7 @@ class _ThumbnailImageState extends State { } return child; }, + placeholderBuilder: widget.heroPlaceholderBuilder, transitionOnUserGestures: true, child: image, ); diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 4d4e4ccfa..e56eee0fd 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -18,6 +18,7 @@ import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/providers/viewer_entry_provider.dart'; import 'package:aves/widgets/viewer/action/video_action_delegate.dart'; import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; @@ -900,6 +901,7 @@ class _EntryViewerStackState extends State with EntryViewContr predicate: (v) => v < 1, animate: false, ); + context.read().value = entry; } }