A head unit is a large screen sitting an arm's length further away than a phone, and Plezy drew phone-sized controls on it: the primary button measured 8.3 mm against the 64 dp a car needs. The whole surface is now scaled - 1.35 by default, adjustable in Appearance - by giving the app a smaller logical viewport and scaling the result back, so text, spacing and touch targets grow together instead of a font size being nudged in isolation. The scale sits above the messenger and the root Scaffold so snackbars and dialogs are scaled too, and insets are divided back into the scaled space so a system bar still reserves its physical size. A scaled surface is also a short one: the setup screen's fixed offsets and the now-playing transport are laid out to survive it, and a mistyped scale in a hand-edited settings file is clamped rather than failing startup.
89 lines
4.2 KiB
Dart
89 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
||
import '../services/settings_service.dart' show LibraryDensity;
|
||
import 'layout_constants.dart';
|
||
import 'platform_detector.dart';
|
||
|
||
class GridSizeCalculator {
|
||
static double _lerp(double min, double max, double t) => min + (max - min) * t;
|
||
|
||
/// Calculates the maximum cross-axis extent for grid items based on screen size and density.
|
||
/// [density] is an int 1–5 (1 = most compact, 5 = most comfortable).
|
||
static double getMaxCrossAxisExtent(BuildContext context, int density) {
|
||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||
final f = LibraryDensity.factor(density);
|
||
|
||
if (PlatformDetector.isTV()) return _lerp(120, 220, f);
|
||
if (ScreenBreakpoints.isDesktopOrLarger(screenWidth)) return _lerp(140, 280, f);
|
||
if (ScreenBreakpoints.isTablet(screenWidth)) return _lerp(120, 230, f);
|
||
return _lerp(100, 200, f);
|
||
}
|
||
|
||
/// Calculates the max cross-axis extent accounting for outer padding.
|
||
/// [density] is an int 1–5.
|
||
static double getMaxCrossAxisExtentWithPadding(BuildContext context, int density, double horizontalPadding) {
|
||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||
final availableWidth = screenWidth - horizontalPadding;
|
||
final f = LibraryDensity.factor(density);
|
||
|
||
// TV-specific sizing for 10ft viewing distance
|
||
if (PlatformDetector.isTV()) {
|
||
final divisor = _lerp(12, 6, f);
|
||
final maxItemWidth = _lerp(140, 240, f);
|
||
return (availableWidth / divisor).clamp(0, maxItemWidth);
|
||
}
|
||
|
||
if (ScreenBreakpoints.isWideTabletOrLarger(screenWidth)) {
|
||
final divisor = _lerp(10, 5, f);
|
||
final maxItemWidth = _lerp(140, 280, f);
|
||
return (availableWidth / divisor).clamp(0, maxItemWidth);
|
||
} else if (ScreenBreakpoints.isTablet(screenWidth)) {
|
||
final targetItemCount = _lerp(6, 3, f);
|
||
return availableWidth / targetItemCount;
|
||
} else {
|
||
final targetItemCount = _lerp(5, 2, f);
|
||
return availableWidth / targetItemCount;
|
||
}
|
||
}
|
||
|
||
/// Calculates the number of columns for a given available width.
|
||
///
|
||
/// Matches Flutter's SliverGridDelegateWithMaxCrossAxisExtent exactly (see
|
||
/// rendering/sliver_grid.dart), so this navigation column count equals the
|
||
/// number of columns the grid actually renders. A mismatch makes dpad "down"
|
||
/// (`index + columnCount`) land diagonally — see issue #1288. Note the spacing
|
||
/// is added to the denominator only, not the numerator:
|
||
/// `(crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil()`
|
||
///
|
||
/// [crossAxisExtent] should come from layout constraints (e.g.
|
||
/// `SliverCrossAxisLayoutBuilder` or `LayoutBuilder`), not from `MediaQuery`,
|
||
/// to account for sidebars or other elements that reduce the grid's actual
|
||
/// width. Never use a plain `SliverLayoutBuilder` for this: its constraints
|
||
/// include the scroll offset, so it rebuilds the whole grid every scroll tick.
|
||
static int getColumnCount(double crossAxisExtent, double maxCrossAxisExtent, {double? crossAxisSpacing}) {
|
||
final effectiveSpacing = crossAxisSpacing ?? GridLayoutConstants.crossAxisSpacing;
|
||
return (crossAxisExtent / (maxCrossAxisExtent + effectiveSpacing)).ceil().clamp(1, 100);
|
||
}
|
||
|
||
static double getCellWidthForColumnCount(double crossAxisExtent, int columnCount, {double? crossAxisSpacing}) {
|
||
final effectiveSpacing = crossAxisSpacing ?? GridLayoutConstants.crossAxisSpacing;
|
||
return (crossAxisExtent - (effectiveSpacing * (columnCount - 1))) / columnCount;
|
||
}
|
||
|
||
/// Computes the actual cell width that a grid with [getMaxCrossAxisExtent] would produce
|
||
/// for the given [availableWidth]. This matches SliverGridDelegateWithMaxCrossAxisExtent's
|
||
/// internal calculation, so horizontal scroll lists can use the same width as grids.
|
||
static double getCellWidth(double availableWidth, BuildContext context, int density) {
|
||
final maxExtent = getMaxCrossAxisExtent(context, density);
|
||
final columns = getColumnCount(availableWidth, maxExtent);
|
||
return getCellWidthForColumnCount(availableWidth, columns);
|
||
}
|
||
|
||
static bool isFirstRow(int index, int columnCount) {
|
||
return index < columnCount;
|
||
}
|
||
|
||
static bool isFirstColumn(int index, int columnCount) {
|
||
return index % columnCount == 0;
|
||
}
|
||
}
|