378 lines
12 KiB
Dart
378 lines
12 KiB
Dart
|
|
// ===== lib/utils/responsive_helper.dart =====
|
||
|
|
// Système de responsivité centralisé pour tablettes et rotation
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter/services.dart';
|
||
|
|
import 'dart:math' as math;
|
||
|
|
|
||
|
|
/// Gestionnaire principal de la responsivité et rotation
|
||
|
|
class ResponsiveHelper {
|
||
|
|
|
||
|
|
// =================== CONSTANTES DE BREAKPOINTS ===================
|
||
|
|
|
||
|
|
/// Breakpoints pour différentes tailles d'écran
|
||
|
|
static const double phoneMaxWidth = 400;
|
||
|
|
static const double tabletMinWidth = 600;
|
||
|
|
static const double desktopMinWidth = 1024;
|
||
|
|
|
||
|
|
/// Hauteurs critiques pour détection d'orientation
|
||
|
|
static const double portraitMinHeight = 600;
|
||
|
|
static const double landscapeMaxHeight = 500;
|
||
|
|
|
||
|
|
// =================== DÉTECTION DE TYPE D'APPAREIL ===================
|
||
|
|
|
||
|
|
/// Détermine si c'est un petit téléphone
|
||
|
|
static bool isSmallPhone(BuildContext context) {
|
||
|
|
return MediaQuery.of(context).size.width < phoneMaxWidth;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détermine si c'est un téléphone standard
|
||
|
|
static bool isPhone(BuildContext context) {
|
||
|
|
return MediaQuery.of(context).size.width < tabletMinWidth;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détermine si c'est une tablette
|
||
|
|
static bool isTablet(BuildContext context) {
|
||
|
|
final width = MediaQuery.of(context).size.width;
|
||
|
|
return width >= tabletMinWidth && width < desktopMinWidth;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détermine si c'est un grand écran/desktop
|
||
|
|
static bool isDesktop(BuildContext context) {
|
||
|
|
return MediaQuery.of(context).size.width >= desktopMinWidth;
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== DÉTECTION D'ORIENTATION ===================
|
||
|
|
|
||
|
|
/// Détection précise de l'orientation
|
||
|
|
static bool isPortrait(BuildContext context) {
|
||
|
|
final size = MediaQuery.of(context).size;
|
||
|
|
return size.height > size.width;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détection du mode paysage
|
||
|
|
static bool isLandscape(BuildContext context) {
|
||
|
|
final size = MediaQuery.of(context).size;
|
||
|
|
return size.width > size.height;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détection spéciale pour tablette en mode paysage
|
||
|
|
static bool isTabletLandscape(BuildContext context) {
|
||
|
|
return isTablet(context) && isLandscape(context);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détection spéciale pour téléphone en mode paysage
|
||
|
|
static bool isPhoneLandscape(BuildContext context) {
|
||
|
|
return isPhone(context) && isLandscape(context);
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== CONFIGURATION D'ORIENTATION ===================
|
||
|
|
|
||
|
|
/// Force l'orientation portrait et paysage pour tablettes
|
||
|
|
static void enableAllOrientations() {
|
||
|
|
SystemChrome.setPreferredOrientations([
|
||
|
|
DeviceOrientation.portraitUp,
|
||
|
|
DeviceOrientation.portraitDown,
|
||
|
|
DeviceOrientation.landscapeLeft,
|
||
|
|
DeviceOrientation.landscapeRight,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Force uniquement le mode portrait (pour certains écrans si nécessaire)
|
||
|
|
static void enablePortraitOnly() {
|
||
|
|
SystemChrome.setPreferredOrientations([
|
||
|
|
DeviceOrientation.portraitUp,
|
||
|
|
DeviceOrientation.portraitDown,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Force uniquement le mode paysage
|
||
|
|
static void enableLandscapeOnly() {
|
||
|
|
SystemChrome.setPreferredOrientations([
|
||
|
|
DeviceOrientation.landscapeLeft,
|
||
|
|
DeviceOrientation.landscapeRight,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== DIMENSIONS RESPONSIVES ===================
|
||
|
|
|
||
|
|
/// Obtient la largeur responsive selon l'appareil
|
||
|
|
static double getResponsiveWidth(BuildContext context, {
|
||
|
|
double? phone,
|
||
|
|
double? tablet,
|
||
|
|
double? desktop,
|
||
|
|
}) {
|
||
|
|
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? 0;
|
||
|
|
if (isTablet(context)) return tablet ?? phone ?? 0;
|
||
|
|
return phone ?? 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Obtient la hauteur responsive selon l'appareil
|
||
|
|
static double getResponsiveHeight(BuildContext context, {
|
||
|
|
double? phone,
|
||
|
|
double? tablet,
|
||
|
|
double? desktop,
|
||
|
|
}) {
|
||
|
|
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? 0;
|
||
|
|
if (isTablet(context)) return tablet ?? phone ?? 0;
|
||
|
|
return phone ?? 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Padding responsive adaptatif
|
||
|
|
static EdgeInsets getResponsivePadding(BuildContext context, {
|
||
|
|
EdgeInsets? phone,
|
||
|
|
EdgeInsets? tablet,
|
||
|
|
EdgeInsets? desktop,
|
||
|
|
}) {
|
||
|
|
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? EdgeInsets.zero;
|
||
|
|
if (isTablet(context)) return tablet ?? phone ?? EdgeInsets.zero;
|
||
|
|
return phone ?? EdgeInsets.zero;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Taille de police responsive
|
||
|
|
static double getResponsiveFontSize(BuildContext context, {
|
||
|
|
double? phone,
|
||
|
|
double? tablet,
|
||
|
|
double? desktop,
|
||
|
|
}) {
|
||
|
|
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? 14;
|
||
|
|
if (isTablet(context)) return tablet ?? phone ?? 14;
|
||
|
|
return phone ?? 14;
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== GRILLES RESPONSIVES ===================
|
||
|
|
|
||
|
|
/// Nombre de colonnes selon la taille d'écran
|
||
|
|
static int getGridColumns(BuildContext context, {
|
||
|
|
int phoneColumns = 2,
|
||
|
|
int tabletPortraitColumns = 3,
|
||
|
|
int tabletLandscapeColumns = 4,
|
||
|
|
int desktopColumns = 5,
|
||
|
|
}) {
|
||
|
|
if (isDesktop(context)) return desktopColumns;
|
||
|
|
if (isTabletLandscape(context)) return tabletLandscapeColumns;
|
||
|
|
if (isTablet(context)) return tabletPortraitColumns;
|
||
|
|
return phoneColumns;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ratio d'aspect adaptatif pour les cartes
|
||
|
|
static double getCardAspectRatio(BuildContext context) {
|
||
|
|
if (isPhoneLandscape(context)) return 2.5;
|
||
|
|
if (isTabletLandscape(context)) return 1.8;
|
||
|
|
if (isTablet(context)) return 1.4;
|
||
|
|
return 1.2; // Portrait par défaut
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== WIDGETS RESPONSIVES ===================
|
||
|
|
|
||
|
|
/// Container responsive avec padding adaptatif
|
||
|
|
static Widget responsiveContainer({
|
||
|
|
required BuildContext context,
|
||
|
|
required Widget child,
|
||
|
|
EdgeInsets? phonePadding,
|
||
|
|
EdgeInsets? tabletPadding,
|
||
|
|
EdgeInsets? desktopPadding,
|
||
|
|
Color? backgroundColor,
|
||
|
|
BorderRadius? borderRadius,
|
||
|
|
}) {
|
||
|
|
return Container(
|
||
|
|
padding: getResponsivePadding(
|
||
|
|
context,
|
||
|
|
phone: phonePadding ?? const EdgeInsets.all(16),
|
||
|
|
tablet: tabletPadding ?? const EdgeInsets.all(24),
|
||
|
|
desktop: desktopPadding ?? const EdgeInsets.all(32),
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: backgroundColor,
|
||
|
|
borderRadius: borderRadius ?? BorderRadius.circular(12),
|
||
|
|
),
|
||
|
|
child: child,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Texte responsive avec tailles adaptatives
|
||
|
|
static Widget responsiveText(
|
||
|
|
String text, {
|
||
|
|
required BuildContext context,
|
||
|
|
double? phoneSize,
|
||
|
|
double? tabletSize,
|
||
|
|
double? desktopSize,
|
||
|
|
FontWeight? fontWeight,
|
||
|
|
Color? color,
|
||
|
|
TextAlign? textAlign,
|
||
|
|
int? maxLines,
|
||
|
|
TextOverflow? overflow,
|
||
|
|
}) {
|
||
|
|
return Text(
|
||
|
|
text,
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: getResponsiveFontSize(
|
||
|
|
context,
|
||
|
|
phone: phoneSize ?? 14,
|
||
|
|
tablet: tabletSize ?? 16,
|
||
|
|
desktop: desktopSize ?? 18,
|
||
|
|
),
|
||
|
|
fontWeight: fontWeight,
|
||
|
|
color: color,
|
||
|
|
),
|
||
|
|
textAlign: textAlign,
|
||
|
|
maxLines: maxLines,
|
||
|
|
overflow: overflow,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== LAYOUTS RESPONSIVES ===================
|
||
|
|
|
||
|
|
/// Layout responsive en colonnes
|
||
|
|
static Widget responsiveRow({
|
||
|
|
required BuildContext context,
|
||
|
|
required List<Widget> children,
|
||
|
|
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
|
||
|
|
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
|
||
|
|
bool forceColumnOnPhone = true,
|
||
|
|
}) {
|
||
|
|
if (forceColumnOnPhone && isPhone(context)) {
|
||
|
|
return Column(
|
||
|
|
mainAxisAlignment: mainAxisAlignment,
|
||
|
|
crossAxisAlignment: crossAxisAlignment,
|
||
|
|
children: children,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return Row(
|
||
|
|
mainAxisAlignment: mainAxisAlignment,
|
||
|
|
crossAxisAlignment: crossAxisAlignment,
|
||
|
|
children: children,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// GridView responsive adaptatif
|
||
|
|
static Widget responsiveGridView({
|
||
|
|
required BuildContext context,
|
||
|
|
required List<Widget> children,
|
||
|
|
int? phoneColumns,
|
||
|
|
int? tabletPortraitColumns,
|
||
|
|
int? tabletLandscapeColumns,
|
||
|
|
int? desktopColumns,
|
||
|
|
double childAspectRatio = 1.0,
|
||
|
|
double crossAxisSpacing = 8.0,
|
||
|
|
double mainAxisSpacing = 8.0,
|
||
|
|
EdgeInsets? padding,
|
||
|
|
ScrollPhysics? physics,
|
||
|
|
}) {
|
||
|
|
final columns = getGridColumns(
|
||
|
|
context,
|
||
|
|
phoneColumns: phoneColumns ?? 2,
|
||
|
|
tabletPortraitColumns: tabletPortraitColumns ?? 3,
|
||
|
|
tabletLandscapeColumns: tabletLandscapeColumns ?? 4,
|
||
|
|
desktopColumns: desktopColumns ?? 5,
|
||
|
|
);
|
||
|
|
|
||
|
|
return GridView.count(
|
||
|
|
shrinkWrap: true,
|
||
|
|
physics: physics ?? const NeverScrollableScrollPhysics(),
|
||
|
|
crossAxisCount: columns,
|
||
|
|
childAspectRatio: childAspectRatio,
|
||
|
|
crossAxisSpacing: crossAxisSpacing,
|
||
|
|
mainAxisSpacing: mainAxisSpacing,
|
||
|
|
padding: padding ?? getResponsivePadding(
|
||
|
|
context,
|
||
|
|
phone: const EdgeInsets.all(16),
|
||
|
|
tablet: const EdgeInsets.all(24),
|
||
|
|
),
|
||
|
|
children: children,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== NAVIGATION RESPONSIVE ===================
|
||
|
|
|
||
|
|
/// Détermine si on doit utiliser un Drawer ou une BottomNavigationBar
|
||
|
|
static bool shouldUseDrawer(BuildContext context) {
|
||
|
|
return isTabletLandscape(context) || isDesktop(context);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détermine si on doit utiliser une navigation rail (côté gauche)
|
||
|
|
static bool shouldUseNavigationRail(BuildContext context) {
|
||
|
|
return isTabletLandscape(context) || isDesktop(context);
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== ORIENTATIONS SPÉCIFIQUES ===================
|
||
|
|
|
||
|
|
/// Initialise les orientations selon l'appareil
|
||
|
|
static void initializeOrientations(BuildContext context) {
|
||
|
|
if (isTablet(context) || isDesktop(context)) {
|
||
|
|
enableAllOrientations();
|
||
|
|
} else {
|
||
|
|
// Pour les téléphones, on peut choisir de limiter ou non
|
||
|
|
enableAllOrientations(); // Ou enablePortraitOnly() selon les besoins
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== UTILITAIRES DIVERS ===================
|
||
|
|
|
||
|
|
/// Obtient la largeur maximale recommandée pour le contenu
|
||
|
|
static double getMaxContentWidth(BuildContext context) {
|
||
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
||
|
|
|
||
|
|
if (isDesktop(context)) {
|
||
|
|
return math.min(screenWidth * 0.8, 1200); // Max 1200px sur desktop
|
||
|
|
} else if (isTablet(context)) {
|
||
|
|
return screenWidth * 0.9; // 90% sur tablette
|
||
|
|
}
|
||
|
|
return screenWidth; // 100% sur téléphone
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Centre le contenu avec une largeur maximale
|
||
|
|
static Widget centerContent({
|
||
|
|
required BuildContext context,
|
||
|
|
required Widget child,
|
||
|
|
double? maxWidth,
|
||
|
|
}) {
|
||
|
|
return Center(
|
||
|
|
child: ConstrainedBox(
|
||
|
|
constraints: BoxConstraints(
|
||
|
|
maxWidth: maxWidth ?? getMaxContentWidth(context),
|
||
|
|
),
|
||
|
|
child: child,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Détermine l'espacement entre les éléments
|
||
|
|
static double getSpacing(BuildContext context, {
|
||
|
|
double? small,
|
||
|
|
double? medium,
|
||
|
|
double? large,
|
||
|
|
}) {
|
||
|
|
if (isDesktop(context)) return large ?? 24;
|
||
|
|
if (isTablet(context)) return medium ?? 16;
|
||
|
|
return small ?? 12;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// =================== EXTENSION POUR MEDIAQUERY ===================
|
||
|
|
|
||
|
|
/// Extension pour simplifier l'utilisation de ResponsiveHelper
|
||
|
|
extension ResponsiveExtension on BuildContext {
|
||
|
|
bool get isSmallPhone => ResponsiveHelper.isSmallPhone(this);
|
||
|
|
bool get isPhone => ResponsiveHelper.isPhone(this);
|
||
|
|
bool get isTablet => ResponsiveHelper.isTablet(this);
|
||
|
|
bool get isDesktop => ResponsiveHelper.isDesktop(this);
|
||
|
|
bool get isPortrait => ResponsiveHelper.isPortrait(this);
|
||
|
|
bool get isLandscape => ResponsiveHelper.isLandscape(this);
|
||
|
|
bool get isTabletLandscape => ResponsiveHelper.isTabletLandscape(this);
|
||
|
|
bool get isPhoneLandscape => ResponsiveHelper.isPhoneLandscape(this);
|
||
|
|
|
||
|
|
double responsiveWidth({double? phone, double? tablet, double? desktop}) =>
|
||
|
|
ResponsiveHelper.getResponsiveWidth(this, phone: phone, tablet: tablet, desktop: desktop);
|
||
|
|
|
||
|
|
double responsiveHeight({double? phone, double? tablet, double? desktop}) =>
|
||
|
|
ResponsiveHelper.getResponsiveHeight(this, phone: phone, tablet: tablet, desktop: desktop);
|
||
|
|
|
||
|
|
double responsiveFontSize({double? phone, double? tablet, double? desktop}) =>
|
||
|
|
ResponsiveHelper.getResponsiveFontSize(this, phone: phone, tablet: tablet, desktop: desktop);
|
||
|
|
|
||
|
|
EdgeInsets responsivePadding({EdgeInsets? phone, EdgeInsets? tablet, EdgeInsets? desktop}) =>
|
||
|
|
ResponsiveHelper.getResponsivePadding(this, phone: phone, tablet: tablet, desktop: desktop);
|
||
|
|
}
|
||
|
|
|