#1174 custom placeholder handling for collection-viewer hero

This commit is contained in:
Thibault Deckers 2024-09-16 00:07:14 +02:00
parent cc3b4f661b
commit f4e5018b78
7 changed files with 67 additions and 3 deletions

View file

@ -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/durations_provider.dart';
import 'package:aves/widgets/common/providers/highlight_info_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/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/home_page.dart';
import 'package:aves/widgets/navigation/tv_page_transitions.dart'; import 'package:aves/widgets/navigation/tv_page_transitions.dart';
import 'package:aves/widgets/navigation/tv_rail.dart'; import 'package:aves/widgets/navigation/tv_rail.dart';
@ -224,6 +225,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Provider<TvRailController>.value(value: _tvRailController), Provider<TvRailController>.value(value: _tvRailController),
DurationsProvider(), DurationsProvider(),
HighlightInfoProvider(), HighlightInfoProvider(),
ViewerEntryProvider(),
], ],
child: NotificationListener<PopExitNotification>( child: NotificationListener<PopExitNotification>(
onNotification: (notification) { onNotification: (notification) {

View file

@ -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/empty.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.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/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/decorated.dart';
import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/thumbnail/notifications.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:aves_model/aves_model.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -116,6 +118,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
final ValueNotifier<AppMode> _selectingAppModeNotifier = ValueNotifier(AppMode.pickFilteredMediaInternal); final ValueNotifier<AppMode> _selectingAppModeNotifier = ValueNotifier(AppMode.pickFilteredMediaInternal);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => context.read<ViewerEntryNotifier>().value = null);
}
@override @override
void dispose() { void dispose() {
_focusedItemNotifier.dispose(); _focusedItemNotifier.dispose();
@ -238,9 +246,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
); );
} }
void _goToViewer(CollectionLens collection, AvesEntry entry) { Future<void> _goToViewer(CollectionLens collection, AvesEntry entry) async {
// track viewer entry for dynamic hero placeholder
WidgetsBinding.instance.addPostFrameCallback((_) => context.read<ViewerEntryNotifier>().value = entry);
final selection = context.read<Selection<AvesEntry>>(); final selection = context.read<Selection<AvesEntry>>();
Navigator.maybeOf(context)?.push( await Navigator.maybeOf(context)?.push(
TransparentMaterialPageRoute( TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName), settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) { pageBuilder: (context, a, sa) {
@ -266,6 +277,14 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
}, },
), ),
); );
// reset track viewer entry
final animate = context.read<Settings>().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<ViewerEntryNotifier>().value = null;
} }
} }

View file

@ -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.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/grid/scaling.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/decorated.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart'; import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/hero.dart';
@ -124,5 +125,20 @@ class Tile extends StatelessWidget {
selectable: selectable, selectable: selectable,
highlightable: highlightable, highlightable: highlightable,
heroTagger: heroTagger, 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<ViewerEntryNotifier, bool>(
selector: (context, v) => v.value == entry,
builder: (context, isViewerEntry, child) {
return Visibility.maintain(
visible: !isViewerEntry,
child: child!,
);
},
child: child,
);
},
); );
} }

View file

@ -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<ViewerEntryNotifier> {
ViewerEntryProvider({
super.key,
super.child,
}) : super(
create: (context) => ViewerEntryNotifier(null),
dispose: (context, value) => value.dispose(),
);
}
class ViewerEntryNotifier extends ValueNotifier<AvesEntry?> {
ViewerEntryNotifier(super.value);
}

View file

@ -13,6 +13,8 @@ class DecoratedThumbnail extends StatelessWidget {
final ValueNotifier<bool>? cancellableNotifier; final ValueNotifier<bool>? cancellableNotifier;
final bool isMosaic, selectable, highlightable; final bool isMosaic, selectable, highlightable;
final Object? Function()? heroTagger; final Object? Function()? heroTagger;
final HeroPlaceholderBuilder? heroPlaceholderBuilder;
final TransitionBuilder? imageDecorator;
static Color borderColor(BuildContext context) => Theme.of(context).dividerColor; static Color borderColor(BuildContext context) => Theme.of(context).dividerColor;
@ -27,6 +29,8 @@ class DecoratedThumbnail extends StatelessWidget {
this.selectable = true, this.selectable = true,
this.highlightable = true, this.highlightable = true,
this.heroTagger, this.heroTagger,
this.heroPlaceholderBuilder,
this.imageDecorator,
}); });
@override @override
@ -50,12 +54,13 @@ class DecoratedThumbnail extends StatelessWidget {
isMosaic: isMosaic, isMosaic: isMosaic,
cancellableNotifier: cancellableNotifier, cancellableNotifier: cancellableNotifier,
heroTag: heroTagger?.call(), heroTag: heroTagger?.call(),
heroPlaceholderBuilder: heroPlaceholderBuilder,
); );
child = Stack( child = Stack(
fit: StackFit.passthrough, fit: StackFit.passthrough,
children: [ children: [
child, imageDecorator?.call(context, child) ?? child,
ThumbnailEntryOverlay(entry: entry), ThumbnailEntryOverlay(entry: entry),
if (selectable) ...[ if (selectable) ...[
GridItemSelectionOverlay<AvesEntry>( GridItemSelectionOverlay<AvesEntry>(

View file

@ -25,6 +25,7 @@ class ThumbnailImage extends StatefulWidget {
final bool showLoadingBackground; final bool showLoadingBackground;
final ValueNotifier<bool>? cancellableNotifier; final ValueNotifier<bool>? cancellableNotifier;
final Object? heroTag; final Object? heroTag;
final HeroPlaceholderBuilder? heroPlaceholderBuilder;
const ThumbnailImage({ const ThumbnailImage({
super.key, super.key,
@ -37,6 +38,7 @@ class ThumbnailImage extends StatefulWidget {
this.showLoadingBackground = true, this.showLoadingBackground = true,
this.cancellableNotifier, this.cancellableNotifier,
this.heroTag, this.heroTag,
this.heroPlaceholderBuilder,
}); });
@override @override
@ -283,6 +285,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
} }
return child; return child;
}, },
placeholderBuilder: widget.heroPlaceholderBuilder,
transitionOnUserGestures: true, transitionOnUserGestures: true,
child: image, child: image,
); );

View file

@ -18,6 +18,7 @@ import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.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/action/video_action_delegate.dart';
import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart';
@ -900,6 +901,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
predicate: (v) => v < 1, predicate: (v) => v < 1,
animate: false, animate: false,
); );
context.read<ViewerEntryNotifier>().value = entry;
} }
} }