From 3ca33d0608cdac6175ca370506333048862e6085 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 13 Dec 2022 15:20:08 +0100 Subject: [PATCH] #437 tv: grid item scale on focus --- lib/theme/durations.dart | 5 ++ lib/widgets/collection/collection_grid.dart | 45 ++++++++++- lib/widgets/common/grid/scaling.dart | 5 +- lib/widgets/common/map/buttons/panel.dart | 3 +- .../filter_grids/common/filter_grid_page.dart | 79 ++++++++++++++----- 5 files changed, 112 insertions(+), 25 deletions(-) diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 0bf51f1ea..770124e4f 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -105,6 +105,9 @@ class DurationsData { final Duration staggeredAnimationPageTarget; final Duration quickChooserAnimation; + // grid animations + final Duration gridTvFocusAnimation; + // viewer animations final Duration viewerVerticalPageScrollAnimation; final Duration viewerOverlayAnimation; @@ -123,6 +126,7 @@ class DurationsData { this.staggeredAnimation = const Duration(milliseconds: 375), this.staggeredAnimationPageTarget = const Duration(milliseconds: 800), this.quickChooserAnimation = const Duration(milliseconds: 100), + this.gridTvFocusAnimation = const Duration(milliseconds: 150), this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500), this.viewerOverlayAnimation = const Duration(milliseconds: 200), this.viewerOverlayChangeAnimation = const Duration(milliseconds: 150), @@ -140,6 +144,7 @@ class DurationsData { staggeredAnimation: Duration.zero, staggeredAnimationPageTarget: Duration.zero, quickChooserAnimation: Duration.zero, + gridTvFocusAnimation: Duration.zero, viewerVerticalPageScrollAnimation: Duration.zero, viewerOverlayAnimation: Duration.zero, viewerOverlayChangeAnimation: Duration.zero, diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index c75778274..76e1368f0 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; +import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/favourite.dart'; @@ -92,14 +93,29 @@ class _CollectionGridState extends State { } return TileExtentControllerProvider( controller: _tileExtentController!, - child: _CollectionGridContent(), + child: const _CollectionGridContent(), ); } } -class _CollectionGridContent extends StatelessWidget { +class _CollectionGridContent extends StatefulWidget { + const _CollectionGridContent(); + + @override + State<_CollectionGridContent> createState() => _CollectionGridContentState(); +} + +class _CollectionGridContentState extends State<_CollectionGridContent> { + final ValueNotifier _focusedItemNotifier = ValueNotifier(null); final ValueNotifier _isScrollingNotifier = ValueNotifier(false); + @override + void dispose() { + _focusedItemNotifier.dispose(); + _isScrollingNotifier.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final selectable = context.select, bool>((v) => v.value.canSelectMedia); @@ -151,7 +167,7 @@ class _CollectionGridContent extends StatelessWidget { return AnimatedBuilder( animation: favourites, builder: (context, child) { - return InteractiveTile( + Widget tile = InteractiveTile( key: ValueKey(entry.id), collection: collection, entry: entry, @@ -159,6 +175,29 @@ class _CollectionGridContent extends StatelessWidget { tileLayout: tileLayout, isScrollingNotifier: _isScrollingNotifier, ); + if (!device.isTelevision) return tile; + + return Focus( + onFocusChange: (focused) { + if (focused) { + _focusedItemNotifier.value = entry; + } else if (_focusedItemNotifier.value == entry) { + _focusedItemNotifier.value = null; + } + }, + child: ValueListenableBuilder( + valueListenable: _focusedItemNotifier, + builder: (context, focusedItem, child) { + return AnimatedScale( + scale: focusedItem == entry ? 1 : .9, + curve: Curves.fastOutSlowIn, + duration: context.select((v) => v.gridTvFocusAnimation), + child: child!, + ); + }, + child: tile, + ), + ); }, ); }, diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index cb3bb8cbc..8cf5ec376 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -109,7 +109,10 @@ class _GridScaleGestureDetectorState extends State) return; + _metadata = metadata; + switch (tileLayout) { case TileLayout.mosaic: _startSize = Size.square(tileExtentController.extentNotifier.value); diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index faf9f8418..58df61ba1 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -85,12 +85,11 @@ class MapButtonPanel extends StatelessWidget { builder: (context, bounds, child) { final degrees = bounds.rotation; final opacity = degrees == 0 ? .0 : 1.0; - final animationDuration = context.select((v) => v.viewerOverlayAnimation); return IgnorePointer( ignoring: opacity == 0, child: AnimatedOpacity( opacity: opacity, - duration: animationDuration, + duration: context.select((v) => v.viewerOverlayAnimation), child: MapOverlayButton( icon: Transform( origin: iconSize.center(Offset.zero), diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index f030636fd..2e178f3fc 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -233,8 +233,9 @@ class _FilterGridState extends State> } } -class _FilterGridContent extends StatelessWidget { +class _FilterGridContent extends StatefulWidget { final Widget appBar; + final double appBarHeight; final Map>> sections; final Set newFilters; final ChipSortFactor sortFactor; @@ -243,12 +244,10 @@ class _FilterGridContent extends StatelessWidget { final QueryTest applyQuery; final HeroType heroType; - final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); - - _FilterGridContent({ + const _FilterGridContent({ super.key, required this.appBar, - required double appBarHeight, + required this.appBarHeight, required this.sections, required this.newFilters, required this.sortFactor, @@ -257,8 +256,27 @@ class _FilterGridContent extends StatelessWidget { required this.applyQuery, required this.emptyBuilder, required this.heroType, - }) { - _appBarHeightNotifier.value = appBarHeight; + }); + + @override + State<_FilterGridContent> createState() => _FilterGridContentState(); +} + +class _FilterGridContentState extends State<_FilterGridContent> { + final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); + final ValueNotifier?> _focusedItemNotifier = ValueNotifier(null); + + @override + void didUpdateWidget(covariant _FilterGridContent oldWidget) { + super.didUpdateWidget(oldWidget); + _appBarHeightNotifier.value = widget.appBarHeight; + } + + @override + void dispose() { + _appBarHeightNotifier.dispose(); + _focusedItemNotifier.dispose(); + super.dispose(); } @override @@ -275,14 +293,14 @@ class _FilterGridContent extends StatelessWidget { Map>> visibleSections; if (queryEnabled && query.isNotEmpty) { visibleSections = {}; - sections.forEach((sectionKey, sectionFilters) { - final visibleFilters = applyQuery(context, sectionFilters, query.toUpperCase()); + widget.sections.forEach((sectionKey, sectionFilters) { + final visibleFilters = widget.applyQuery(context, sectionFilters, query.toUpperCase()); if (visibleFilters.isNotEmpty) { visibleSections[sectionKey] = visibleFilters; } }); } else { - visibleSections = sections; + visibleSections = widget.sections; } final sectionedListLayoutProvider = ValueListenableBuilder( @@ -312,8 +330,8 @@ class _FilterGridContent extends StatelessWidget { extent: thumbnailExtent, child: SectionedFilterListLayoutProvider( sections: visibleSections, - showHeaders: showHeaders, - selectable: selectable, + showHeaders: widget.showHeaders, + selectable: widget.selectable, tileLayout: tileLayout, scrollableWidth: scrollableWidth, columnCount: columnCount, @@ -323,13 +341,36 @@ class _FilterGridContent extends StatelessWidget { tileHeight: tileHeight, tileBuilder: (gridItem, tileSize) { final extent = tileSize.shortestSide; - return InteractiveFilterTile( + final tile = InteractiveFilterTile( gridItem: gridItem, chipExtent: extent, thumbnailExtent: extent, tileLayout: tileLayout, banner: _getFilterBanner(context, gridItem.filter), - heroType: heroType, + heroType: widget.heroType, + ); + if (!device.isTelevision) return tile; + + return Focus( + onFocusChange: (focused) { + if (focused) { + _focusedItemNotifier.value = gridItem; + } else if (_focusedItemNotifier.value == gridItem) { + _focusedItemNotifier.value = null; + } + }, + child: ValueListenableBuilder?>( + valueListenable: _focusedItemNotifier, + builder: (context, focusedItem, child) { + return AnimatedScale( + scale: focusedItem == gridItem ? 1 : .9, + curve: Curves.fastOutSlowIn, + duration: context.select((v) => v.gridTvFocusAnimation), + child: child!, + ); + }, + child: tile, + ), ); }, tileAnimationDelay: tileAnimationDelay, @@ -349,12 +390,12 @@ class _FilterGridContent extends StatelessWidget { ); }, child: _FilterSectionedContent( - appBar: appBar, + appBar: widget.appBar, appBarHeightNotifier: _appBarHeightNotifier, visibleSections: visibleSections, - sortFactor: sortFactor, - selectable: selectable, - emptyBuilder: emptyBuilder, + sortFactor: widget.sortFactor, + selectable: widget.selectable, + emptyBuilder: widget.emptyBuilder, bannerBuilder: _getFilterBanner, scrollController: PrimaryScrollController.of(context)!, tileLayout: tileLayout, @@ -368,7 +409,7 @@ class _FilterGridContent extends StatelessWidget { } String? _getFilterBanner(BuildContext context, T filter) { - final isNew = newFilters.contains(filter); + final isNew = widget.newFilters.contains(filter); return isNew ? context.l10n.newFilterBanner : null; } }