#437 tv: grid item scale on focus

This commit is contained in:
Thibault Deckers 2022-12-13 15:20:08 +01:00
parent 85ec850e03
commit 3ca33d0608
5 changed files with 112 additions and 25 deletions

View file

@ -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,

View file

@ -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<CollectionGrid> {
}
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<AvesEntry?> _focusedItemNotifier = ValueNotifier(null);
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
@override
void dispose() {
_focusedItemNotifier.dispose();
_isScrollingNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final selectable = context.select<ValueNotifier<AppMode>, 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<AvesEntry?>(
valueListenable: _focusedItemNotifier,
builder: (context, focusedItem, child) {
return AnimatedScale(
scale: focusedItem == entry ? 1 : .9,
curve: Curves.fastOutSlowIn,
duration: context.select<DurationsData, Duration>((v) => v.gridTvFocusAnimation),
child: child!,
);
},
child: tile,
),
);
},
);
},

View file

@ -109,7 +109,10 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
);
// abort if we cannot find an image to show on overlay
if (renderMetaData == null) return;
_metadata = renderMetaData.metaData;
final metadata = renderMetaData.metaData;
if (metadata is! ScalerMetadata<T>) return;
_metadata = metadata;
switch (tileLayout) {
case TileLayout.mosaic:
_startSize = Size.square(tileExtentController.extentNotifier.value);

View file

@ -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<DurationsData, Duration>((v) => v.viewerOverlayAnimation);
return IgnorePointer(
ignoring: opacity == 0,
child: AnimatedOpacity(
opacity: opacity,
duration: animationDuration,
duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayAnimation),
child: MapOverlayButton(
icon: Transform(
origin: iconSize.center(Offset.zero),

View file

@ -233,8 +233,9 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
}
}
class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
class _FilterGridContent<T extends CollectionFilter> extends StatefulWidget {
final Widget appBar;
final double appBarHeight;
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
final Set<T> newFilters;
final ChipSortFactor sortFactor;
@ -243,12 +244,10 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
final QueryTest<T> applyQuery;
final HeroType heroType;
final ValueNotifier<double> _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<T extends CollectionFilter> extends StatelessWidget {
required this.applyQuery,
required this.emptyBuilder,
required this.heroType,
}) {
_appBarHeightNotifier.value = appBarHeight;
});
@override
State<_FilterGridContent<T>> createState() => _FilterGridContentState<T>();
}
class _FilterGridContentState<T extends CollectionFilter> extends State<_FilterGridContent<T>> {
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<FilterGridItem<T>?> _focusedItemNotifier = ValueNotifier(null);
@override
void didUpdateWidget(covariant _FilterGridContent<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_appBarHeightNotifier.value = widget.appBarHeight;
}
@override
void dispose() {
_appBarHeightNotifier.dispose();
_focusedItemNotifier.dispose();
super.dispose();
}
@override
@ -275,14 +293,14 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
Map<ChipSectionKey, List<FilterGridItem<T>>> 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<double>(
@ -312,8 +330,8 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
extent: thumbnailExtent,
child: SectionedFilterListLayoutProvider<T>(
sections: visibleSections,
showHeaders: showHeaders,
selectable: selectable,
showHeaders: widget.showHeaders,
selectable: widget.selectable,
tileLayout: tileLayout,
scrollableWidth: scrollableWidth,
columnCount: columnCount,
@ -323,13 +341,36 @@ class _FilterGridContent<T extends CollectionFilter> 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<FilterGridItem<T>?>(
valueListenable: _focusedItemNotifier,
builder: (context, focusedItem, child) {
return AnimatedScale(
scale: focusedItem == gridItem ? 1 : .9,
curve: Curves.fastOutSlowIn,
duration: context.select<DurationsData, Duration>((v) => v.gridTvFocusAnimation),
child: child!,
);
},
child: tile,
),
);
},
tileAnimationDelay: tileAnimationDelay,
@ -349,12 +390,12 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
);
},
child: _FilterSectionedContent<T>(
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<T extends CollectionFilter> 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;
}
}