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.
mohemm-flutter-app/lib/widgets/responsive/duo_responsive.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,
),
);
},
);
}
}