#437 tv: grid item scale on focus
This commit is contained in:
parent
85ec850e03
commit
3ca33d0608
5 changed files with 112 additions and 25 deletions
|
@ -105,6 +105,9 @@ class DurationsData {
|
||||||
final Duration staggeredAnimationPageTarget;
|
final Duration staggeredAnimationPageTarget;
|
||||||
final Duration quickChooserAnimation;
|
final Duration quickChooserAnimation;
|
||||||
|
|
||||||
|
// grid animations
|
||||||
|
final Duration gridTvFocusAnimation;
|
||||||
|
|
||||||
// viewer animations
|
// viewer animations
|
||||||
final Duration viewerVerticalPageScrollAnimation;
|
final Duration viewerVerticalPageScrollAnimation;
|
||||||
final Duration viewerOverlayAnimation;
|
final Duration viewerOverlayAnimation;
|
||||||
|
@ -123,6 +126,7 @@ class DurationsData {
|
||||||
this.staggeredAnimation = const Duration(milliseconds: 375),
|
this.staggeredAnimation = const Duration(milliseconds: 375),
|
||||||
this.staggeredAnimationPageTarget = const Duration(milliseconds: 800),
|
this.staggeredAnimationPageTarget = const Duration(milliseconds: 800),
|
||||||
this.quickChooserAnimation = const Duration(milliseconds: 100),
|
this.quickChooserAnimation = const Duration(milliseconds: 100),
|
||||||
|
this.gridTvFocusAnimation = const Duration(milliseconds: 150),
|
||||||
this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500),
|
this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500),
|
||||||
this.viewerOverlayAnimation = const Duration(milliseconds: 200),
|
this.viewerOverlayAnimation = const Duration(milliseconds: 200),
|
||||||
this.viewerOverlayChangeAnimation = const Duration(milliseconds: 150),
|
this.viewerOverlayChangeAnimation = const Duration(milliseconds: 150),
|
||||||
|
@ -140,6 +144,7 @@ class DurationsData {
|
||||||
staggeredAnimation: Duration.zero,
|
staggeredAnimation: Duration.zero,
|
||||||
staggeredAnimationPageTarget: Duration.zero,
|
staggeredAnimationPageTarget: Duration.zero,
|
||||||
quickChooserAnimation: Duration.zero,
|
quickChooserAnimation: Duration.zero,
|
||||||
|
gridTvFocusAnimation: Duration.zero,
|
||||||
viewerVerticalPageScrollAnimation: Duration.zero,
|
viewerVerticalPageScrollAnimation: Duration.zero,
|
||||||
viewerOverlayAnimation: Duration.zero,
|
viewerOverlayAnimation: Duration.zero,
|
||||||
viewerOverlayChangeAnimation: Duration.zero,
|
viewerOverlayChangeAnimation: Duration.zero,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
|
@ -92,14 +93,29 @@ class _CollectionGridState extends State<CollectionGrid> {
|
||||||
}
|
}
|
||||||
return TileExtentControllerProvider(
|
return TileExtentControllerProvider(
|
||||||
controller: _tileExtentController!,
|
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);
|
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusedItemNotifier.dispose();
|
||||||
|
_isScrollingNotifier.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final selectable = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canSelectMedia);
|
final selectable = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canSelectMedia);
|
||||||
|
@ -151,7 +167,7 @@ class _CollectionGridContent extends StatelessWidget {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: favourites,
|
animation: favourites,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return InteractiveTile(
|
Widget tile = InteractiveTile(
|
||||||
key: ValueKey(entry.id),
|
key: ValueKey(entry.id),
|
||||||
collection: collection,
|
collection: collection,
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
@ -159,6 +175,29 @@ class _CollectionGridContent extends StatelessWidget {
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
isScrollingNotifier: _isScrollingNotifier,
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -109,7 +109,10 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
);
|
);
|
||||||
// abort if we cannot find an image to show on overlay
|
// abort if we cannot find an image to show on overlay
|
||||||
if (renderMetaData == null) return;
|
if (renderMetaData == null) return;
|
||||||
_metadata = renderMetaData.metaData;
|
final metadata = renderMetaData.metaData;
|
||||||
|
if (metadata is! ScalerMetadata<T>) return;
|
||||||
|
_metadata = metadata;
|
||||||
|
|
||||||
switch (tileLayout) {
|
switch (tileLayout) {
|
||||||
case TileLayout.mosaic:
|
case TileLayout.mosaic:
|
||||||
_startSize = Size.square(tileExtentController.extentNotifier.value);
|
_startSize = Size.square(tileExtentController.extentNotifier.value);
|
||||||
|
|
|
@ -85,12 +85,11 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
builder: (context, bounds, child) {
|
builder: (context, bounds, child) {
|
||||||
final degrees = bounds.rotation;
|
final degrees = bounds.rotation;
|
||||||
final opacity = degrees == 0 ? .0 : 1.0;
|
final opacity = degrees == 0 ? .0 : 1.0;
|
||||||
final animationDuration = context.select<DurationsData, Duration>((v) => v.viewerOverlayAnimation);
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: opacity == 0,
|
ignoring: opacity == 0,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
opacity: opacity,
|
opacity: opacity,
|
||||||
duration: animationDuration,
|
duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayAnimation),
|
||||||
child: MapOverlayButton(
|
child: MapOverlayButton(
|
||||||
icon: Transform(
|
icon: Transform(
|
||||||
origin: iconSize.center(Offset.zero),
|
origin: iconSize.center(Offset.zero),
|
||||||
|
|
|
@ -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 Widget appBar;
|
||||||
|
final double appBarHeight;
|
||||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
|
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
|
||||||
final Set<T> newFilters;
|
final Set<T> newFilters;
|
||||||
final ChipSortFactor sortFactor;
|
final ChipSortFactor sortFactor;
|
||||||
|
@ -243,12 +244,10 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final QueryTest<T> applyQuery;
|
final QueryTest<T> applyQuery;
|
||||||
final HeroType heroType;
|
final HeroType heroType;
|
||||||
|
|
||||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
const _FilterGridContent({
|
||||||
|
|
||||||
_FilterGridContent({
|
|
||||||
super.key,
|
super.key,
|
||||||
required this.appBar,
|
required this.appBar,
|
||||||
required double appBarHeight,
|
required this.appBarHeight,
|
||||||
required this.sections,
|
required this.sections,
|
||||||
required this.newFilters,
|
required this.newFilters,
|
||||||
required this.sortFactor,
|
required this.sortFactor,
|
||||||
|
@ -257,8 +256,27 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
required this.applyQuery,
|
required this.applyQuery,
|
||||||
required this.emptyBuilder,
|
required this.emptyBuilder,
|
||||||
required this.heroType,
|
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
|
@override
|
||||||
|
@ -275,14 +293,14 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
Map<ChipSectionKey, List<FilterGridItem<T>>> visibleSections;
|
Map<ChipSectionKey, List<FilterGridItem<T>>> visibleSections;
|
||||||
if (queryEnabled && query.isNotEmpty) {
|
if (queryEnabled && query.isNotEmpty) {
|
||||||
visibleSections = {};
|
visibleSections = {};
|
||||||
sections.forEach((sectionKey, sectionFilters) {
|
widget.sections.forEach((sectionKey, sectionFilters) {
|
||||||
final visibleFilters = applyQuery(context, sectionFilters, query.toUpperCase());
|
final visibleFilters = widget.applyQuery(context, sectionFilters, query.toUpperCase());
|
||||||
if (visibleFilters.isNotEmpty) {
|
if (visibleFilters.isNotEmpty) {
|
||||||
visibleSections[sectionKey] = visibleFilters;
|
visibleSections[sectionKey] = visibleFilters;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
visibleSections = sections;
|
visibleSections = widget.sections;
|
||||||
}
|
}
|
||||||
|
|
||||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||||
|
@ -312,8 +330,8 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
extent: thumbnailExtent,
|
extent: thumbnailExtent,
|
||||||
child: SectionedFilterListLayoutProvider<T>(
|
child: SectionedFilterListLayoutProvider<T>(
|
||||||
sections: visibleSections,
|
sections: visibleSections,
|
||||||
showHeaders: showHeaders,
|
showHeaders: widget.showHeaders,
|
||||||
selectable: selectable,
|
selectable: widget.selectable,
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
scrollableWidth: scrollableWidth,
|
scrollableWidth: scrollableWidth,
|
||||||
columnCount: columnCount,
|
columnCount: columnCount,
|
||||||
|
@ -323,13 +341,36 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
tileHeight: tileHeight,
|
tileHeight: tileHeight,
|
||||||
tileBuilder: (gridItem, tileSize) {
|
tileBuilder: (gridItem, tileSize) {
|
||||||
final extent = tileSize.shortestSide;
|
final extent = tileSize.shortestSide;
|
||||||
return InteractiveFilterTile(
|
final tile = InteractiveFilterTile(
|
||||||
gridItem: gridItem,
|
gridItem: gridItem,
|
||||||
chipExtent: extent,
|
chipExtent: extent,
|
||||||
thumbnailExtent: extent,
|
thumbnailExtent: extent,
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
banner: _getFilterBanner(context, gridItem.filter),
|
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,
|
tileAnimationDelay: tileAnimationDelay,
|
||||||
|
@ -349,12 +390,12 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: _FilterSectionedContent<T>(
|
child: _FilterSectionedContent<T>(
|
||||||
appBar: appBar,
|
appBar: widget.appBar,
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
visibleSections: visibleSections,
|
visibleSections: visibleSections,
|
||||||
sortFactor: sortFactor,
|
sortFactor: widget.sortFactor,
|
||||||
selectable: selectable,
|
selectable: widget.selectable,
|
||||||
emptyBuilder: emptyBuilder,
|
emptyBuilder: widget.emptyBuilder,
|
||||||
bannerBuilder: _getFilterBanner,
|
bannerBuilder: _getFilterBanner,
|
||||||
scrollController: PrimaryScrollController.of(context)!,
|
scrollController: PrimaryScrollController.of(context)!,
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
|
@ -368,7 +409,7 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _getFilterBanner(BuildContext context, T filter) {
|
String? _getFilterBanner(BuildContext context, T filter) {
|
||||||
final isNew = newFilters.contains(filter);
|
final isNew = widget.newFilters.contains(filter);
|
||||||
return isNew ? context.l10n.newFilterBanner : null;
|
return isNew ? context.l10n.newFilterBanner : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue