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.
105 lines
3.0 KiB
Dart
105 lines
3.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../utils/platform_detector.dart';
|
|
|
|
class FittingTitleText extends StatelessWidget {
|
|
final String text;
|
|
final TextStyle? style;
|
|
final int maxLines;
|
|
final TextOverflow overflow;
|
|
final TextAlign? textAlign;
|
|
final AlignmentGeometry alignment;
|
|
final double minFontSize;
|
|
|
|
const FittingTitleText(
|
|
this.text, {
|
|
super.key,
|
|
this.style,
|
|
this.maxLines = 2,
|
|
this.overflow = TextOverflow.ellipsis,
|
|
this.textAlign,
|
|
this.alignment = Alignment.centerLeft,
|
|
this.minFontSize = 1,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final baseStyle = style ?? DefaultTextStyle.of(context).style;
|
|
if (PlatformDetector.isAutomotive()) {
|
|
return Align(
|
|
alignment: alignment,
|
|
child: Text(text, style: baseStyle, maxLines: maxLines, overflow: overflow, textAlign: textAlign),
|
|
);
|
|
}
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
var fittedStyle = baseStyle;
|
|
if (constraints.hasBoundedWidth &&
|
|
constraints.hasBoundedHeight &&
|
|
constraints.maxWidth > 0 &&
|
|
constraints.maxHeight > 0) {
|
|
fittedStyle = baseStyle.copyWith(
|
|
fontSize: _fitFontSize(
|
|
text: text,
|
|
style: baseStyle,
|
|
maxWidth: constraints.maxWidth,
|
|
maxHeight: constraints.maxHeight,
|
|
textDirection: Directionality.maybeOf(context) ?? TextDirection.ltr,
|
|
textScaler: MediaQuery.textScalerOf(context),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Align(
|
|
alignment: alignment,
|
|
child: Text(text, style: fittedStyle, maxLines: maxLines, overflow: overflow, textAlign: textAlign),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
double _fitFontSize({
|
|
required String text,
|
|
required TextStyle style,
|
|
required double maxWidth,
|
|
required double maxHeight,
|
|
required TextDirection textDirection,
|
|
required TextScaler textScaler,
|
|
}) {
|
|
final baseFontSize = style.fontSize ?? 14;
|
|
if (baseFontSize <= minFontSize) return baseFontSize;
|
|
|
|
bool fits(double fontSize) {
|
|
final painter = TextPainter(
|
|
text: TextSpan(
|
|
text: text,
|
|
style: style.copyWith(fontSize: fontSize),
|
|
),
|
|
maxLines: maxLines,
|
|
ellipsis: overflow == TextOverflow.ellipsis ? '\u2026' : null,
|
|
textDirection: textDirection,
|
|
textScaler: textScaler,
|
|
textAlign: textAlign ?? TextAlign.start,
|
|
)..layout(maxWidth: maxWidth);
|
|
final result = painter.height <= maxHeight + 0.1 && painter.width <= maxWidth + 0.1;
|
|
painter.dispose();
|
|
return result;
|
|
}
|
|
|
|
if (fits(baseFontSize)) return baseFontSize;
|
|
|
|
if (!fits(minFontSize)) return minFontSize;
|
|
|
|
var low = minFontSize;
|
|
var high = baseFontSize;
|
|
for (var i = 0; i < 12; i++) {
|
|
final mid = (low + high) / 2;
|
|
if (fits(mid)) {
|
|
low = mid;
|
|
} else {
|
|
high = mid;
|
|
}
|
|
}
|
|
return low;
|
|
}
|
|
}
|