You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
4.8 KiB
Dart
134 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Responsive & hinge-aware layout utilities for iPhone Duo (folded, unfolded, and Split View postures).
|
|
class DuoResponsive {
|
|
DuoResponsive._();
|
|
|
|
/// Viewport width threshold (in logical points) where the layout transitions
|
|
/// from folded / narrow Split View to the unfolded inner display posture.
|
|
static const double unfoldedBreakpoint = 600.0;
|
|
|
|
/// Reserved center fold clearance (~40pt) for unfolded dual-pane postures
|
|
/// since MediaQuery.displayFeatures does not natively report the iOS fold yet.
|
|
static const double foldGap = 40.0;
|
|
|
|
/// Maximum width for dialogs and modal bottom sheets so they do not stretch
|
|
/// awkwardly across the 40pt center fold when unfolded.
|
|
static const double maxModalWidth = 420.0;
|
|
|
|
/// Standard capped width for navigation drawers on wide/unfolded screens.
|
|
static const double maxDrawerWidth = 320.0;
|
|
|
|
/// Width of the adaptive side navigation rail when unfolded.
|
|
static const double sideRailWidth = 88.0;
|
|
|
|
/// Returns true when the current window width is in the unfolded / expanded posture.
|
|
static bool isUnfolded(BuildContext context) {
|
|
return MediaQuery.sizeOf(context).width >= unfoldedBreakpoint;
|
|
}
|
|
|
|
/// Returns true when the given constraint or viewport width is in the unfolded posture.
|
|
static bool isWideWidth(double width) {
|
|
return width >= unfoldedBreakpoint;
|
|
}
|
|
|
|
/// Calculates the width of a single pane when split across the center fold gap.
|
|
static double singlePaneWidth(double availableWidth, {double horizontalPadding = 0.0}) {
|
|
double usable = availableWidth - (horizontalPadding * 2);
|
|
if (!isWideWidth(availableWidth)) {
|
|
return usable;
|
|
}
|
|
return ((usable - foldGap) / 2).clamp(0.0, double.infinity);
|
|
}
|
|
|
|
/// Returns adaptive dialog insetPadding so dialogs stay within a single pane
|
|
/// or centered with a comfortable width instead of stretching across the hinge.
|
|
static EdgeInsets dialogInsetPadding(BuildContext context, {double defaultHorizontal = 21.0, double defaultVertical = 24.0}) {
|
|
Size size = MediaQuery.sizeOf(context);
|
|
if (size.width >= unfoldedBreakpoint) {
|
|
double horizontal = ((size.width - maxModalWidth) / 2).clamp(defaultHorizontal, size.width * 0.35);
|
|
return EdgeInsets.symmetric(horizontal: horizontal, vertical: defaultVertical);
|
|
}
|
|
return EdgeInsets.symmetric(horizontal: defaultHorizontal, vertical: defaultVertical);
|
|
}
|
|
|
|
/// Returns BoxConstraints for modal bottom sheets so they never stretch across the fold.
|
|
static BoxConstraints bottomSheetConstraints(BuildContext context, {double? maxHeight}) {
|
|
Size size = MediaQuery.sizeOf(context);
|
|
bool unfolded = size.width >= unfoldedBreakpoint;
|
|
return BoxConstraints(
|
|
maxWidth: unfolded ? maxModalWidth : size.width,
|
|
maxHeight: maxHeight ?? double.infinity,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A two-pane row that automatically inserts [DuoResponsive.foldGap] (~40pt)
|
|
/// between [startPane] and [endPane] when in the unfolded posture, or [compactSpacing]
|
|
/// when in the folded / compact posture.
|
|
class DuoSplitRow extends StatelessWidget {
|
|
final Widget startPane;
|
|
final Widget endPane;
|
|
final double compactSpacing;
|
|
final CrossAxisAlignment crossAxisAlignment;
|
|
|
|
const DuoSplitRow({
|
|
Key? key,
|
|
required this.startPane,
|
|
required this.endPane,
|
|
this.compactSpacing = 9.0,
|
|
this.crossAxisAlignment = CrossAxisAlignment.start,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
bool isWide = DuoResponsive.isWideWidth(constraints.maxWidth) || DuoResponsive.isUnfolded(context);
|
|
double gap = isWide ? DuoResponsive.foldGap : compactSpacing;
|
|
return Row(
|
|
crossAxisAlignment: crossAxisAlignment,
|
|
children: <Widget>[
|
|
Expanded(child: startPane),
|
|
SizedBox(width: gap),
|
|
Expanded(child: endPane),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Constrains wide content (such as forms, cards, or lists) on unfolded screens
|
|
/// while letting it fill the width naturally when folded.
|
|
class DuoContentConstraint extends StatelessWidget {
|
|
final Widget child;
|
|
final double maxWidth;
|
|
final AlignmentGeometry alignment;
|
|
|
|
const DuoContentConstraint({
|
|
Key? key,
|
|
required this.child,
|
|
this.maxWidth = 720.0,
|
|
this.alignment = Alignment.topCenter,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
if (constraints.maxWidth <= maxWidth) {
|
|
return child;
|
|
}
|
|
return Align(
|
|
alignment: alignment,
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|