#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 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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue