correction overlay didacticiel

Origine du problème (BoxConstraints forces an infinite width / RenderPhysicalShape) :
Hauteur non bornée dans le Positioned :
Lorsque la bulle d'aide était positionnée via Positioned(top: ..., bottom: null), Flutter transmettait une contrainte de hauteur infinie (maxHeight: double.infinity).
Le bouton ElevatedButton calcule son ombre et sa forme physique via RenderPhysicalShape en appelant constraints.biggest ; avec une dimension infinie, Flutter générait une contrainte invalide (BoxConstraints(w=Infinity, 50.0<=h<=Infinity)) et crashait le rendu (ce qui provoquait l'écran gris/noir).
Nids de Row imbriqués dans le pied de la carte d'étape.
Solutions apportées :

tutorial_overlay.dart
 :
Hauteur maximale bornée : _buildTooltip calcule et transmet désormais la hauteur disponible réelle de l'écran (availableHeight) via ConstrainedBox(maxHeight: available).
Bouton moderne Material 3 : Remplacement par FilledButton pour le bouton d'action principale ("Suivant" / "C'est parti").
Structure simplifiée : Aplatissement de la barre inférieure avec un Row unique utilisant Spacer().

full_app_render_test.dart
 :
Ajout d'un test d'intégration complet validant le démarrage de l'application et la navigation étape par étape du didacticiel.
This commit is contained in:
streaper2
2026-08-30 11:01:51 +02:00
parent a9651588bb
commit 25caf6ddf8
4 changed files with 235 additions and 69 deletions
@@ -37,7 +37,7 @@ class _TutorialOverlayState extends State<TutorialOverlay>
int _index = 0;
Rect? _spotRect;
bool _ready = false;
late bool _ready = widget.steps.isNotEmpty && widget.steps[0].targetKey == null;
TutorialStep get _step => widget.steps[_index];
bool get _isLast => _index == widget.steps.length - 1;
@@ -63,12 +63,17 @@ class _TutorialOverlayState extends State<TutorialOverlay>
return;
}
await Scrollable.ensureVisible(
targetContext,
alignment: 0.35,
duration: const Duration(milliseconds: 320),
curve: Curves.easeOutCubic,
);
try {
await Scrollable.ensureVisible(
targetContext,
alignment: 0.35,
duration: const Duration(milliseconds: 320),
curve: Curves.easeOutCubic,
);
} catch (_) {
// Ignorer si l'élément n'est pas dans un widget scrollable
}
// Laisse le temps au défilement de se stabiliser avant de mesurer.
await Future<void>.delayed(const Duration(milliseconds: 60));
if (!mounted) return;
@@ -80,13 +85,18 @@ class _TutorialOverlayState extends State<TutorialOverlay>
}
Rect? _measure(BuildContext targetContext, double padding) {
final box = targetContext.findRenderObject() as RenderBox?;
if (box == null || !box.hasSize) return null;
final origin = box.localToGlobal(Offset.zero);
final screen = MediaQuery.of(context).size;
return Rect.fromLTWH(origin.dx, origin.dy, box.size.width, box.size.height)
.inflate(padding)
.intersect(Offset.zero & screen);
if (!targetContext.mounted) return null;
final renderObject = targetContext.findRenderObject();
if (renderObject is! RenderBox || !renderObject.hasSize) return null;
try {
final origin = renderObject.localToGlobal(Offset.zero);
final screen = MediaQuery.of(context).size;
return Rect.fromLTWH(origin.dx, origin.dy, renderObject.size.width, renderObject.size.height)
.inflate(padding)
.intersect(Offset.zero & screen);
} catch (_) {
return null;
}
}
void _next() {
@@ -96,7 +106,7 @@ class _TutorialOverlayState extends State<TutorialOverlay>
}
setState(() {
_index++;
_ready = false;
_ready = widget.steps[_index].targetKey == null;
_spotRect = null;
});
WidgetsBinding.instance.addPostFrameCallback((_) => _prepareStep());
@@ -182,10 +192,14 @@ class _TutorialOverlayState extends State<TutorialOverlay>
);
if (spot == null) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: card,
return Positioned.fill(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Center(
child: card,
),
),
),
);
}
@@ -195,11 +209,19 @@ class _TutorialOverlayState extends State<TutorialOverlay>
// Zone très large (image plein écran) : la bulle flotte en bas de l'écran
// pour laisser la main animée visible au centre.
if (spot.height > screenHeight * 0.55) {
final available = (screenHeight * 0.4).clamp(140.0, screenHeight);
return Positioned(
left: 16,
right: 16,
bottom: media.padding.bottom + 24,
child: card,
child: SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: available),
child: Center(
child: card,
),
),
),
);
}
@@ -207,12 +229,25 @@ class _TutorialOverlayState extends State<TutorialOverlay>
final above =
_step.preferTooltipAbove ?? (spot.top > screenHeight - spot.bottom);
final available = above
? (spot.top - gap - media.padding.top).clamp(140.0, screenHeight)
: (screenHeight - spot.bottom - gap - media.padding.bottom).clamp(140.0, screenHeight);
return Positioned(
left: 16,
right: 16,
top: above ? null : spot.bottom + gap,
top: above ? null : (spot.bottom + gap),
bottom: above ? (screenHeight - spot.top + gap) : null,
child: card,
child: SafeArea(
top: !above,
bottom: above,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: available),
child: Center(
child: card,
),
),
),
);
}
}
@@ -246,10 +281,9 @@ class _TutorialCard extends StatelessWidget {
final textSecondary =
isDark ? AppTheme.darkTextSecondary : AppTheme.lightTextSecondary;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 460),
child: Container(
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 460),
child: Container(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 12),
decoration: BoxDecoration(
color: surface,
@@ -270,7 +304,7 @@ class _TutorialCard extends StatelessWidget {
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
@@ -298,6 +332,7 @@ class _TutorialCard extends StatelessWidget {
),
),
),
const SizedBox(width: 8),
Text(
'${index + 1}/$total',
style: TextStyle(
@@ -339,23 +374,21 @@ class _TutorialCard extends StatelessWidget {
const SizedBox(height: 12),
Row(
children: [
Row(
children: List.generate(total, (i) {
final active = i == index;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(right: 5),
width: active ? 18 : 6,
height: 6,
decoration: BoxDecoration(
color: active
? primary
: primary.withValues(alpha: 0.28),
borderRadius: BorderRadius.circular(3),
),
);
}),
),
...List.generate(total, (i) {
final active = i == index;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(right: 5),
width: active ? 18 : 6,
height: 6,
decoration: BoxDecoration(
color: active
? primary
: primary.withValues(alpha: 0.28),
borderRadius: BorderRadius.circular(3),
),
);
}),
const Spacer(),
TextButton(
onPressed: onSkip,
@@ -363,9 +396,9 @@ class _TutorialCard extends StatelessWidget {
child: const Text('Passer'),
),
const SizedBox(width: 4),
ElevatedButton(
FilledButton(
onPressed: onNext,
style: ElevatedButton.styleFrom(
style: FilledButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
@@ -384,8 +417,7 @@ class _TutorialCard extends StatelessWidget {
],
),
),
),
);
);
}
static String _gestureHint(TutorialGesture gesture) {