Every D-pad step on the TV home ends with a spotlight swap, and on a low-end box that frame ran over budget: 12 % of UI-thread CPU during navigation was paragraph layout, most of it shaping strings a second time to measure them before the render tree shaped them for real. - FittingTitleText no longer lays the title out to learn whether it fits: Text wraps and ellipsizes at maxLines, so the box can only overflow vertically, and one cached one-glyph line height settles that. Only a box shorter than maxLines lines still searches, now bracketed by the proportional shrink of the base layout and stopped at a 0.25 px tolerance instead of a fixed 12 bisections. - FittedMetadataLine and inlineRatingBadgeWidth measure through a small bounded cache keyed by text, style, scaler and direction, so separators, type labels, certifications, runtimes and years measure once per session. - MediaHub.isContinueWatchingHub/usesContinueWatchingAction memoize their regex tokenisation per key; every card build and step re-asked them. Cold navigation on the Android 14 box (30 steps over three hubs): build 312-316 ms -> 284-285 ms, layout 530-540 ms -> 515 ms, UI frame p90 7.5-7.9 ms -> 6.8-7.2 ms.
42 lines
1.5 KiB
Dart
42 lines
1.5 KiB
Dart
import 'dart:collection';
|
|
|
|
import 'package:flutter/painting.dart';
|
|
|
|
typedef _Key = (String, TextStyle, TextScaler, TextDirection);
|
|
|
|
final LinkedHashMap<_Key, Size> _sizes = LinkedHashMap<_Key, Size>();
|
|
const int _maxEntries = 512;
|
|
|
|
/// Size of [text] laid out on one unconstrained line, memoized per
|
|
/// (text, style, scaler, direction).
|
|
///
|
|
/// Metadata strips measure every field before building it so they can shed
|
|
/// parts that will not fit (#1893), and fitted titles need a line height to
|
|
/// know whether a box can overflow at all; each such measurement shapes a
|
|
/// string the render tree is about to shape again. Shaping dominates the TV
|
|
/// spotlight's info swap on low-end boxes, and the measured strings repeat
|
|
/// heavily — separators, "Movie", "TV-14", years, runtimes, "88%" — so a
|
|
/// small cache turns the measuring pass into hash lookups. Bounded and
|
|
/// insertion-ordered: the oldest entry goes first.
|
|
Size cachedSingleLineTextSize(
|
|
String text, {
|
|
required TextStyle style,
|
|
required TextScaler textScaler,
|
|
required TextDirection textDirection,
|
|
}) {
|
|
final key = (text, style, textScaler, textDirection);
|
|
final cached = _sizes[key];
|
|
if (cached != null) return cached;
|
|
final painter = TextPainter(
|
|
text: TextSpan(text: text, style: style),
|
|
textDirection: textDirection,
|
|
textScaler: textScaler,
|
|
maxLines: 1,
|
|
)..layout();
|
|
final size = painter.size;
|
|
painter.dispose();
|
|
if (_sizes.length >= _maxEntries) _sizes.remove(_sizes.keys.first);
|
|
_sizes[key] = size;
|
|
return size;
|
|
}
|