iPhone Duo changes
parent
150f4bf3c7
commit
6066a7a3c2
@ -0,0 +1,133 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue