diff --git a/lib/screens/shared/media/components/next_up_episode.dart b/lib/screens/shared/media/components/next_up_episode.dart index bdb5dea..f27af35 100644 --- a/lib/screens/shared/media/components/next_up_episode.dart +++ b/lib/screens/shared/media/components/next_up_episode.dart @@ -10,6 +10,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sticky_header_text.dart'; import 'package:fladder/util/string_extensions.dart'; +import 'package:fladder/widgets/shared/ensure_visible.dart'; class NextUpEpisode extends ConsumerWidget { final EpisodeModel nextEpisode; @@ -49,6 +50,11 @@ class NextUpEpisode extends ConsumerWidget { showLabel: false, onTap: () => nextEpisode.navigateTo(context), actions: const [], + onFocusChanged: (value) { + if (value) { + context.ensureVisible(); + } + }, isCurrentEpisode: false, ), const SizedBox(height: 16), @@ -71,6 +77,11 @@ class NextUpEpisode extends ConsumerWidget { showLabel: false, onTap: () => nextEpisode.navigateTo(context), actions: const [], + onFocusChanged: (value) { + if (value) { + context.ensureVisible(); + } + }, isCurrentEpisode: false, ), ), diff --git a/lib/screens/shared/media/episode_posters.dart b/lib/screens/shared/media/episode_posters.dart index d61e40b..31156a7 100644 --- a/lib/screens/shared/media/episode_posters.dart +++ b/lib/screens/shared/media/episode_posters.dart @@ -134,6 +134,7 @@ class EpisodePoster extends ConsumerWidget { final Function()? onLongPress; final bool blur; final List actions; + final Function(bool value)? onFocusChanged; final bool isCurrentEpisode; final Object? heroTag; @@ -145,6 +146,7 @@ class EpisodePoster extends ConsumerWidget { this.onLongPress, this.blur = false, required this.actions, + this.onFocusChanged, required this.isCurrentEpisode, this.heroTag, }); @@ -167,6 +169,7 @@ class EpisodePoster extends ConsumerWidget { child: FocusButton( onTap: onTap, onLongPress: onLongPress, + onFocusChanged: onFocusChanged, onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = diff --git a/lib/widgets/shared/ensure_visible.dart b/lib/widgets/shared/ensure_visible.dart index 1f449cf..f687525 100644 --- a/lib/widgets/shared/ensure_visible.dart +++ b/lib/widgets/shared/ensure_visible.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; extension EnsureVisibleHelper on BuildContext { Future ensureVisible({ - Duration duration = const Duration(milliseconds: 225), + Duration duration = const Duration(milliseconds: 275), double? alignment, Curve curve = Curves.fastOutSlowIn, }) { diff --git a/lib/widgets/shared/horizontal_list.dart b/lib/widgets/shared/horizontal_list.dart index 556b679..8b473bc 100644 --- a/lib/widgets/shared/horizontal_list.dart +++ b/lib/widgets/shared/horizontal_list.dart @@ -11,7 +11,6 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/focus_provider.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/sticky_header_text.dart'; -import 'package:fladder/util/throttler.dart'; import 'package:fladder/widgets/navigation_scaffold/components/navigation_body.dart'; import 'package:fladder/widgets/navigation_scaffold/components/side_navigation_bar.dart'; import 'package:fladder/widgets/shared/ensure_visible.dart'; @@ -100,12 +99,11 @@ class _HorizontalListState extends ConsumerState with TickerProv final target = (index * (_firstItemWidth! + contentPadding)).clamp(0, _scrollController.position.maxScrollExtent); - // Cancel any ongoing animation _scrollAnimation?.stop(); final controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 125), + duration: const Duration(milliseconds: 275), ); _scrollAnimation = controller; @@ -115,15 +113,21 @@ class _HorizontalListState extends ConsumerState with TickerProv end: target.toDouble(), ); + final animation = CurvedAnimation( + parent: controller, + curve: Curves.fastOutSlowIn, + ); + controller.addListener(() { if (_scrollController.hasClients) { - _scrollController.jumpTo(tween.evaluate(controller)); + _scrollController.jumpTo(tween.evaluate(animation)); } }); - controller.forward().whenComplete(() { - if (_scrollAnimation == controller) _scrollAnimation = null; - }); + await controller.forward(); + + if (_scrollAnimation == controller) _scrollAnimation = null; + controller.dispose(); } void _scrollToStart() { @@ -272,7 +276,8 @@ class _HorizontalListState extends ConsumerState with TickerProv child: FocusTraversalGroup( policy: HorizontalRailFocus( parentNode: parentNode, - throttle: Throttler(duration: const Duration(milliseconds: 100)), + scrollController: _scrollController, + firstItemWidth: _firstItemWidth ?? 250, onFocused: (node) { lastFocused = node; final correctIndex = _getCorrectIndexForNode(node); @@ -288,6 +293,7 @@ class _HorizontalListState extends ConsumerState with TickerProv clipBehavior: Clip.none, scrollDirection: Axis.horizontal, padding: widget.contentPadding, + cacheExtent: _firstItemWidth ?? 250 * 3, itemBuilder: (context, index) => index == widget.items.length ? PosterPlaceHolder( onTap: widget.onLabelClick ?? () {}, @@ -371,28 +377,33 @@ List _nodesInRow(FocusNode parentNode) { class HorizontalRailFocus extends WidgetOrderTraversalPolicy { final FocusNode parentNode; final void Function(FocusNode node) onFocused; - final Throttler? throttle; + final ScrollController scrollController; + final double firstItemWidth; HorizontalRailFocus({ required this.parentNode, required this.onFocused, - this.throttle, + required this.scrollController, + required this.firstItemWidth, }); @override bool inDirection(FocusNode currentNode, TraversalDirection direction) { final rowNodes = _nodesInRow(parentNode); final index = rowNodes.indexOf(currentNode); + if (index == -1) return false; if (direction == TraversalDirection.left) { - if (throttle?.canRun() == false) return true; + if (scrollController.hasClients && scrollController.offset <= firstItemWidth * 0.5) { + lastMainFocus = currentNode; + navBarNode.requestFocus(); + return true; + } + if (index > 0) { final target = rowNodes[index - 1]; target.requestFocus(); onFocused(target); - } else { - lastMainFocus = currentNode; - navBarNode.requestFocus(); } return true; }