mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
fix: Improved logic horizontal list navigation
This commit is contained in:
parent
b4e68c9e15
commit
47128eb3b6
4 changed files with 40 additions and 15 deletions
|
|
@ -10,6 +10,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/util/sticky_header_text.dart';
|
import 'package:fladder/util/sticky_header_text.dart';
|
||||||
import 'package:fladder/util/string_extensions.dart';
|
import 'package:fladder/util/string_extensions.dart';
|
||||||
|
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||||
|
|
||||||
class NextUpEpisode extends ConsumerWidget {
|
class NextUpEpisode extends ConsumerWidget {
|
||||||
final EpisodeModel nextEpisode;
|
final EpisodeModel nextEpisode;
|
||||||
|
|
@ -49,6 +50,11 @@ class NextUpEpisode extends ConsumerWidget {
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
onTap: () => nextEpisode.navigateTo(context),
|
onTap: () => nextEpisode.navigateTo(context),
|
||||||
actions: const [],
|
actions: const [],
|
||||||
|
onFocusChanged: (value) {
|
||||||
|
if (value) {
|
||||||
|
context.ensureVisible();
|
||||||
|
}
|
||||||
|
},
|
||||||
isCurrentEpisode: false,
|
isCurrentEpisode: false,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -71,6 +77,11 @@ class NextUpEpisode extends ConsumerWidget {
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
onTap: () => nextEpisode.navigateTo(context),
|
onTap: () => nextEpisode.navigateTo(context),
|
||||||
actions: const [],
|
actions: const [],
|
||||||
|
onFocusChanged: (value) {
|
||||||
|
if (value) {
|
||||||
|
context.ensureVisible();
|
||||||
|
}
|
||||||
|
},
|
||||||
isCurrentEpisode: false,
|
isCurrentEpisode: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ class EpisodePoster extends ConsumerWidget {
|
||||||
final Function()? onLongPress;
|
final Function()? onLongPress;
|
||||||
final bool blur;
|
final bool blur;
|
||||||
final List<ItemAction> actions;
|
final List<ItemAction> actions;
|
||||||
|
final Function(bool value)? onFocusChanged;
|
||||||
final bool isCurrentEpisode;
|
final bool isCurrentEpisode;
|
||||||
final Object? heroTag;
|
final Object? heroTag;
|
||||||
|
|
||||||
|
|
@ -145,6 +146,7 @@ class EpisodePoster extends ConsumerWidget {
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.blur = false,
|
this.blur = false,
|
||||||
required this.actions,
|
required this.actions,
|
||||||
|
this.onFocusChanged,
|
||||||
required this.isCurrentEpisode,
|
required this.isCurrentEpisode,
|
||||||
this.heroTag,
|
this.heroTag,
|
||||||
});
|
});
|
||||||
|
|
@ -167,6 +169,7 @@ class EpisodePoster extends ConsumerWidget {
|
||||||
child: FocusButton(
|
child: FocusButton(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
|
onFocusChanged: onFocusChanged,
|
||||||
onSecondaryTapDown: (details) async {
|
onSecondaryTapDown: (details) async {
|
||||||
Offset localPosition = details.globalPosition;
|
Offset localPosition = details.globalPosition;
|
||||||
RelativeRect position =
|
RelativeRect position =
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension EnsureVisibleHelper on BuildContext {
|
extension EnsureVisibleHelper on BuildContext {
|
||||||
Future<void> ensureVisible({
|
Future<void> ensureVisible({
|
||||||
Duration duration = const Duration(milliseconds: 225),
|
Duration duration = const Duration(milliseconds: 275),
|
||||||
double? alignment,
|
double? alignment,
|
||||||
Curve curve = Curves.fastOutSlowIn,
|
Curve curve = Curves.fastOutSlowIn,
|
||||||
}) {
|
}) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/focus_provider.dart';
|
import 'package:fladder/util/focus_provider.dart';
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
import 'package:fladder/util/sticky_header_text.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/navigation_body.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/side_navigation_bar.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/side_navigation_bar.dart';
|
||||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||||
|
|
@ -100,12 +99,11 @@ class _HorizontalListState extends ConsumerState<HorizontalList> with TickerProv
|
||||||
|
|
||||||
final target = (index * (_firstItemWidth! + contentPadding)).clamp(0, _scrollController.position.maxScrollExtent);
|
final target = (index * (_firstItemWidth! + contentPadding)).clamp(0, _scrollController.position.maxScrollExtent);
|
||||||
|
|
||||||
// Cancel any ongoing animation
|
|
||||||
_scrollAnimation?.stop();
|
_scrollAnimation?.stop();
|
||||||
|
|
||||||
final controller = AnimationController(
|
final controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 125),
|
duration: const Duration(milliseconds: 275),
|
||||||
);
|
);
|
||||||
|
|
||||||
_scrollAnimation = controller;
|
_scrollAnimation = controller;
|
||||||
|
|
@ -115,15 +113,21 @@ class _HorizontalListState extends ConsumerState<HorizontalList> with TickerProv
|
||||||
end: target.toDouble(),
|
end: target.toDouble(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final animation = CurvedAnimation(
|
||||||
|
parent: controller,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
);
|
||||||
|
|
||||||
controller.addListener(() {
|
controller.addListener(() {
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
_scrollController.jumpTo(tween.evaluate(controller));
|
_scrollController.jumpTo(tween.evaluate(animation));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
controller.forward().whenComplete(() {
|
await controller.forward();
|
||||||
if (_scrollAnimation == controller) _scrollAnimation = null;
|
|
||||||
});
|
if (_scrollAnimation == controller) _scrollAnimation = null;
|
||||||
|
controller.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollToStart() {
|
void _scrollToStart() {
|
||||||
|
|
@ -272,7 +276,8 @@ class _HorizontalListState extends ConsumerState<HorizontalList> with TickerProv
|
||||||
child: FocusTraversalGroup(
|
child: FocusTraversalGroup(
|
||||||
policy: HorizontalRailFocus(
|
policy: HorizontalRailFocus(
|
||||||
parentNode: parentNode,
|
parentNode: parentNode,
|
||||||
throttle: Throttler(duration: const Duration(milliseconds: 100)),
|
scrollController: _scrollController,
|
||||||
|
firstItemWidth: _firstItemWidth ?? 250,
|
||||||
onFocused: (node) {
|
onFocused: (node) {
|
||||||
lastFocused = node;
|
lastFocused = node;
|
||||||
final correctIndex = _getCorrectIndexForNode(node);
|
final correctIndex = _getCorrectIndexForNode(node);
|
||||||
|
|
@ -288,6 +293,7 @@ class _HorizontalListState extends ConsumerState<HorizontalList> with TickerProv
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: widget.contentPadding,
|
padding: widget.contentPadding,
|
||||||
|
cacheExtent: _firstItemWidth ?? 250 * 3,
|
||||||
itemBuilder: (context, index) => index == widget.items.length
|
itemBuilder: (context, index) => index == widget.items.length
|
||||||
? PosterPlaceHolder(
|
? PosterPlaceHolder(
|
||||||
onTap: widget.onLabelClick ?? () {},
|
onTap: widget.onLabelClick ?? () {},
|
||||||
|
|
@ -371,28 +377,33 @@ List<FocusNode> _nodesInRow(FocusNode parentNode) {
|
||||||
class HorizontalRailFocus extends WidgetOrderTraversalPolicy {
|
class HorizontalRailFocus extends WidgetOrderTraversalPolicy {
|
||||||
final FocusNode parentNode;
|
final FocusNode parentNode;
|
||||||
final void Function(FocusNode node) onFocused;
|
final void Function(FocusNode node) onFocused;
|
||||||
final Throttler? throttle;
|
final ScrollController scrollController;
|
||||||
|
final double firstItemWidth;
|
||||||
|
|
||||||
HorizontalRailFocus({
|
HorizontalRailFocus({
|
||||||
required this.parentNode,
|
required this.parentNode,
|
||||||
required this.onFocused,
|
required this.onFocused,
|
||||||
this.throttle,
|
required this.scrollController,
|
||||||
|
required this.firstItemWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool inDirection(FocusNode currentNode, TraversalDirection direction) {
|
bool inDirection(FocusNode currentNode, TraversalDirection direction) {
|
||||||
final rowNodes = _nodesInRow(parentNode);
|
final rowNodes = _nodesInRow(parentNode);
|
||||||
final index = rowNodes.indexOf(currentNode);
|
final index = rowNodes.indexOf(currentNode);
|
||||||
|
if (index == -1) return false;
|
||||||
|
|
||||||
if (direction == TraversalDirection.left) {
|
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) {
|
if (index > 0) {
|
||||||
final target = rowNodes[index - 1];
|
final target = rowNodes[index - 1];
|
||||||
target.requestFocus();
|
target.requestFocus();
|
||||||
onFocused(target);
|
onFocused(target);
|
||||||
} else {
|
|
||||||
lastMainFocus = currentNode;
|
|
||||||
navBarNode.requestFocus();
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue