Files
SpotiFLAC-Mobile/lib/utils/adaptive_layout.dart
T

113 lines
4.3 KiB
Dart

import 'package:flutter/material.dart';
/// Keeps touch-first tablets on the bottom navigation used by phones. A rail
/// is reserved for genuinely desktop-width windows; the old 600dp threshold
/// made an iPad portrait layout look like a shrunken desktop app.
const double navigationRailBreakpoint = 1440;
const double defaultWideContentMaxWidth = 960;
const double maxWideContentInset = 32;
bool useNavigationRailForWidth(double width) =>
width >= navigationRailBreakpoint;
/// Explicit horizontal safe spacing for controls drawn in full-bleed detail
/// headers. iOS reports no left/right safe-area inset in portrait, so toolbar
/// buttons otherwise land only a few pixels from the device edge.
double detailHeaderEdgeInset(BuildContext context) {
final platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS || platform == TargetPlatform.macOS
? 12
: 0;
}
/// Visual scale for touch-first tablet surfaces. Large iPads have enough
/// pixels to make an unscaled phone density feel miniature, especially when
/// Simulator shrinks the whole device to fit a desktop display.
double adaptiveUiScaleForSize(Size size) {
if (size.shortestSide < 600 || size.width >= navigationRailBreakpoint) {
return 1;
}
return size.shortestSide >= 800 ? 1.2 : 1.1;
}
EdgeInsets _scaledInsets(EdgeInsets value, double divisor) => EdgeInsets.only(
left: value.left / divisor,
top: value.top / divisor,
right: value.right / divisor,
bottom: value.bottom / divisor,
);
/// Scales the complete app surface on tablets, including controls, dialogs,
/// navigation, spacing and hit targets. The child receives the correspondingly
/// smaller logical viewport so responsive layouts still reflow instead of
/// being cropped, while [FittedBox] maps that viewport back to the full screen.
class AdaptiveUiScaler extends StatelessWidget {
const AdaptiveUiScaler({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final scale = adaptiveUiScaleForSize(mediaQuery.size);
if (scale == 1) return child;
final scaledSize = Size(
mediaQuery.size.width / scale,
mediaQuery.size.height / scale,
);
final scaledMediaQuery = mediaQuery.copyWith(
size: scaledSize,
// One inner logical pixel occupies [scale] outer logical pixels.
// Advertising the effective density keeps decoded artwork sharp.
devicePixelRatio: mediaQuery.devicePixelRatio * scale,
padding: _scaledInsets(mediaQuery.padding, scale),
viewPadding: _scaledInsets(mediaQuery.viewPadding, scale),
viewInsets: _scaledInsets(mediaQuery.viewInsets, scale),
systemGestureInsets: _scaledInsets(mediaQuery.systemGestureInsets, scale),
);
return SizedBox.expand(
child: FittedBox(
fit: BoxFit.fill,
alignment: Alignment.topLeft,
child: SizedBox.fromSize(
size: scaledSize,
child: MediaQuery(data: scaledMediaQuery, child: child),
),
),
);
}
}
/// Widest content span for a surface of [maxWidth]: content is never narrower
/// than [contentMaxWidth], and the centering margin never exceeds 32dp per
/// side so tablets retain near-full-width rows.
double adaptiveContentMaxWidth(
double maxWidth, {
double contentMaxWidth = defaultWideContentMaxWidth,
}) => maxWidth > contentMaxWidth + (maxWideContentInset * 2)
? maxWidth - (maxWideContentInset * 2)
: contentMaxWidth;
/// Horizontal inset that centers content of [maxWidth] at
/// [adaptiveContentMaxWidth]; zero once it fits. Prefer this constraint-based
/// form when the widget may live inside an already clamped box (e.g. a bottom
/// sheet), where screen width would over-inset.
double wideInsetForWidth(
double maxWidth, {
double contentMaxWidth = defaultWideContentMaxWidth,
}) => maxWidth > contentMaxWidth
? ((maxWidth - contentMaxWidth) / 2).clamp(0.0, maxWideContentInset)
: 0;
/// Horizontal inset that centers full-width list content on tablets/landscape;
/// zero on phones, so rows stop stretching across the whole screen.
double wideListInset(
BuildContext context, {
double contentMaxWidth = defaultWideContentMaxWidth,
}) => wideInsetForWidth(
MediaQuery.sizeOf(context).width,
contentMaxWidth: contentMaxWidth,
);