import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// Paramètres de découpe 9-slice (schéma extensible.png). class NineSliceSpec { /// Zone centrale extensible en pixels source (LTRB du centre). final Rect centerSlice; /// Largeur logique fixe des coins gauche / droite. final double cornerWidth; /// Hauteur logique fixe des coins haut / bas. final double cornerHeight; const NineSliceSpec({ required this.centerSlice, required this.cornerWidth, required this.cornerHeight, }); /// Bulles / cartes horizontales `card_*_h.png` (~1024×512). static const cards = NineSliceSpec( centerSlice: Rect.fromLTRB(160, 90, 864, 422), cornerWidth: 56, cornerHeight: 40, ); /// Bandeaux couple `bandeau_*.png` (~1024×160). static const bandeau = NineSliceSpec( centerSlice: Rect.fromLTRB(140, 32, 884, 128), cornerWidth: 56, cornerHeight: 28, ); @override bool operator ==(Object other) => identical(this, other) || other is NineSliceSpec && centerSlice == other.centerSlice && cornerWidth == other.cornerWidth && cornerHeight == other.cornerHeight; @override int get hashCode => Object.hash(centerSlice, cornerWidth, cornerHeight); } /// Fond pastel extensible réutilisable (cartes, bandeau couple, etc.). /// /// Coins à taille logique fixe ; bords H/V + centre s’étirent. /// Pas de fichier `.9.png` Android. class NineSliceAsset extends StatefulWidget { final String assetPath; final NineSliceSpec spec; final Widget? child; final EdgeInsetsGeometry? padding; final BoxConstraints? constraints; final double? width; final double? height; const NineSliceAsset({ super.key, required this.assetPath, this.spec = NineSliceSpec.cards, this.child, this.padding, this.constraints, this.width, this.height, }); /// Raccourci pour les bulles de la file d’attention. const NineSliceAsset.card({ super.key, required this.assetPath, this.child, this.padding, this.constraints, this.width, this.height, }) : spec = NineSliceSpec.cards; /// Raccourci pour le bandeau choix enfant / nounou (ou parents). const NineSliceAsset.bandeau({ super.key, required this.assetPath, this.child, this.padding, this.constraints, this.width, this.height, }) : spec = NineSliceSpec.bandeau; @override State createState() => _NineSliceAssetState(); } /// Cache partagé pour éviter de recharger le même PNG à chaque bulle. class _NineSliceImageCache { static final Map> _futures = {}; static Future load(String path) { return _futures.putIfAbsent(path, () async { final data = await rootBundle.load(path); final codec = await ui.instantiateImageCodec( data.buffer.asUint8List(), ); final frame = await codec.getNextFrame(); return frame.image; }); } } class _NineSliceAssetState extends State { ui.Image? _image; String? _loadedPath; @override void initState() { super.initState(); _load(); } @override void didUpdateWidget(covariant NineSliceAsset oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.assetPath != widget.assetPath) { _load(); } } Future _load() async { final path = widget.assetPath; final image = await _NineSliceImageCache.load(path); if (!mounted) return; setState(() { _image = image; _loadedPath = path; }); } @override Widget build(BuildContext context) { final img = _image; final ready = img != null && _loadedPath == widget.assetPath; return SizedBox( width: widget.width, height: widget.height, child: Container( constraints: widget.constraints, child: CustomPaint( painter: ready ? _NineSlicePainter( image: img!, spec: widget.spec, ) : null, child: Padding( padding: widget.padding ?? EdgeInsets.zero, child: widget.child, ), ), ), ); } } class _NineSlicePainter extends CustomPainter { final ui.Image image; final NineSliceSpec spec; _NineSlicePainter({ required this.image, required this.spec, }); @override void paint(Canvas canvas, Size size) { if (size.width <= 0 || size.height <= 0) return; final srcW = image.width.toDouble(); final srcH = image.height.toDouble(); final centerSlice = spec.centerSlice; final srcLeft = centerSlice.left.clamp(0.0, srcW); final srcTop = centerSlice.top.clamp(0.0, srcH); final srcRight = centerSlice.right.clamp(0.0, srcW); final srcBottom = centerSlice.bottom.clamp(0.0, srcH); final srcLeftW = srcLeft; final srcTopH = srcTop; final srcRightW = srcW - srcRight; final srcBottomH = srcH - srcBottom; final srcCenterW = (srcRight - srcLeft).clamp(1.0, srcW); final srcCenterH = (srcBottom - srcTop).clamp(1.0, srcH); final dstLeftW = spec.cornerWidth.clamp(1.0, size.width / 2); final dstRightW = spec.cornerWidth.clamp(1.0, size.width / 2); final dstTopH = spec.cornerHeight.clamp(1.0, size.height / 2); final dstBottomH = spec.cornerHeight.clamp(1.0, size.height / 2); final dstCenterW = (size.width - dstLeftW - dstRightW).clamp(0.0, size.width); final dstCenterH = (size.height - dstTopH - dstBottomH).clamp(0.0, size.height); final paint = Paint()..filterQuality = FilterQuality.high; void draw(Rect src, Rect dst) { if (src.width <= 0 || src.height <= 0) return; if (dst.width <= 0 || dst.height <= 0) return; canvas.drawImageRect(image, src, dst, paint); } // Haut draw( Rect.fromLTWH(0, 0, srcLeftW, srcTopH), Rect.fromLTWH(0, 0, dstLeftW, dstTopH), ); draw( Rect.fromLTWH(srcLeft, 0, srcCenterW, srcTopH), Rect.fromLTWH(dstLeftW, 0, dstCenterW, dstTopH), ); draw( Rect.fromLTWH(srcRight, 0, srcRightW, srcTopH), Rect.fromLTWH(dstLeftW + dstCenterW, 0, dstRightW, dstTopH), ); // Milieu draw( Rect.fromLTWH(0, srcTop, srcLeftW, srcCenterH), Rect.fromLTWH(0, dstTopH, dstLeftW, dstCenterH), ); draw( Rect.fromLTWH(srcLeft, srcTop, srcCenterW, srcCenterH), Rect.fromLTWH(dstLeftW, dstTopH, dstCenterW, dstCenterH), ); draw( Rect.fromLTWH(srcRight, srcTop, srcRightW, srcCenterH), Rect.fromLTWH(dstLeftW + dstCenterW, dstTopH, dstRightW, dstCenterH), ); // Bas draw( Rect.fromLTWH(0, srcBottom, srcLeftW, srcBottomH), Rect.fromLTWH(0, dstTopH + dstCenterH, dstLeftW, dstBottomH), ); draw( Rect.fromLTWH(srcLeft, srcBottom, srcCenterW, srcBottomH), Rect.fromLTWH(dstLeftW, dstTopH + dstCenterH, dstCenterW, dstBottomH), ); draw( Rect.fromLTWH(srcRight, srcBottom, srcRightW, srcBottomH), Rect.fromLTWH( dstLeftW + dstCenterW, dstTopH + dstCenterH, dstRightW, dstBottomH, ), ); } @override bool shouldRepaint(covariant _NineSlicePainter oldDelegate) { return oldDelegate.image != image || oldDelegate.spec != spec; } }