fix(tv): symmetric full-card focus glow via overlay

close #1231
This commit is contained in:
edde746
2026-06-06 05:09:07 +02:00
parent 6b3e49d410
commit d90cc61541
7 changed files with 759 additions and 129 deletions
+23 -10
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../focus/focus_glow_overlay.dart';
import '../focus/focus_theme.dart';
import '../focus/input_mode_tracker.dart';
import 'clickable_cursor.dart';
@@ -77,6 +78,7 @@ class FocusBuilders {
double focusBorderStrokeAlign = BorderSide.strokeAlignInside,
bool useFocusGlow = false,
bool useForegroundFocusDecoration = false,
Size? glowSize,
required Widget child,
}) {
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
@@ -103,21 +105,30 @@ class FocusBuilders {
borderRadius: borderRadius,
borderStrokeAlign: focusBorderStrokeAlign,
);
final glowDecoration = useFocusGlow
? FocusTheme.focusGlowDecoration(context, isFocused: showFocus, borderRadius: borderRadius)
: null;
// Glow (full-bleed cards) renders in an overlay above siblings so it stays
// symmetric; the in-card decoration only carries the border.
Widget card = AnimatedContainer(
duration: duration,
curve: Curves.easeOutCubic,
decoration: useForegroundFocusDecoration ? null : focusDecoration,
foregroundDecoration: useForegroundFocusDecoration ? focusDecoration : null,
child: child,
);
if (useFocusGlow) {
card = FocusGlowOverlay(
isFocused: showFocus,
borderRadius: borderRadius,
color: FocusTheme.getFocusBorderColor(context),
glowSize: glowSize,
child: card,
);
}
final focusedWidget = AnimatedScale(
scale: showFocus ? focusScale : 1.0,
duration: duration,
curve: Curves.easeOutCubic,
child: AnimatedContainer(
duration: duration,
curve: Curves.easeOutCubic,
decoration: useForegroundFocusDecoration ? glowDecoration : focusDecoration,
foregroundDecoration: useForegroundFocusDecoration ? focusDecoration : null,
child: child,
),
child: card,
);
// Wrap in GestureDetector if tap/long press handlers provided
@@ -156,6 +167,7 @@ class FocusBuilders {
double focusBorderStrokeAlign = BorderSide.strokeAlignInside,
bool useFocusGlow = false,
bool useForegroundFocusDecoration = false,
Size? glowSize,
required Widget child,
}) {
return buildFocusableCard(
@@ -170,6 +182,7 @@ class FocusBuilders {
focusBorderStrokeAlign: focusBorderStrokeAlign,
useFocusGlow: useFocusGlow,
useForegroundFocusDecoration: useForegroundFocusDecoration,
glowSize: glowSize,
child: child,
);
}
+4 -23
View File
@@ -79,15 +79,6 @@ class TvBrowseRailLayout {
static double viewAllPillHeightForScale(double scale) => (44 * scale).clamp(36, 54).toDouble();
static double fullCardFocusPaintOverflowForScale(double scale) {
return (FocusTheme.focusGlowOuterBlurRadius +
FocusTheme.focusGlowSpreadRadius +
FocusTheme.focusBorderWidth +
(10 * scale))
.clamp(42, 64)
.toDouble();
}
static double hubStripHeightForScale(double scale) => 36 * scale;
static double hubStripGapForScale(double _) => 0;
@@ -979,9 +970,6 @@ class TvBrowseRailState extends State<TvBrowseRail> {
TvBrowseRailLayout.railTopPaddingForScale(scale) +
viewportHeight +
TvBrowseRailLayout.railBottomPaddingForScale(scale);
final paintOverflow = fullCardLayout && hasFocus
? TvBrowseRailLayout.fullCardFocusPaintOverflowForScale(scale)
: 0.0;
return Focus(
focusNode: _focusNode,
onKeyEvent: _handleKeyEvent,
@@ -1010,12 +998,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
duration: FocusTheme.getAnimationDuration(context),
curve: Curves.easeOutCubic,
child: ClipRect(
clipper: _RailClipper(
leftOverflow: horizontalInset,
rightOverflow: paintOverflow,
topOverflow: 0,
bottomOverflow: paintOverflow,
),
clipper: _RailClipper(leftOverflow: horizontalInset, rightOverflow: 0),
child: SizedBox(
height: viewportHeight,
child: _buildHubSectionList(
@@ -1164,9 +1147,6 @@ class TvBrowseRailState extends State<TvBrowseRail> {
final inactiveIndex = HubFocusMemory.getForHubOnly(hub.id, totalCount);
final focusedIndex = isActiveHub ? _itemIndex : inactiveIndex;
final scrollController = _scrollControllerForHub(hub, metrics, railViewportWidth, scale, focusedIndex);
final paintOverflow = fullCardLayout && hasFocus && isActiveHub
? TvBrowseRailLayout.fullCardFocusPaintOverflowForScale(scale)
: 0.0;
_metricsByHub[hub.id] = metrics;
_scaleByHub[hub.id] = scale;
@@ -1178,8 +1158,8 @@ class TvBrowseRailState extends State<TvBrowseRail> {
child: ClipRect(
clipper: _RailClipper(
leftOverflow: leftOverflow,
rightOverflow: metrics.railEdgePadding + metrics.cardWidth + metrics.itemGap + paintOverflow,
verticalOverflow: fullCardLayout ? math.max(metrics.focusExtra, paintOverflow) : metrics.focusExtra,
rightOverflow: metrics.railEdgePadding + metrics.cardWidth + metrics.itemGap,
verticalOverflow: metrics.focusExtra,
),
child: HorizontalScrollWithArrows(
controller: scrollController,
@@ -1217,6 +1197,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
focusBorderStrokeAlign: fullCardLayout ? BorderSide.strokeAlignOutside : BorderSide.strokeAlignInside,
useFocusGlow: fullCardLayout,
useForegroundFocusDecoration: fullCardLayout,
glowSize: fullCardLayout ? Size(metrics.cardWidth, metrics.posterHeight) : null,
onTap: () {
_selectHubItem(hub, hubIndex, itemIndex);
unawaited(_activateCurrentItem());