import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// Fond 9-slice dynamique (schéma extensible.png). /// /// - Coins : taille logique fixe (ne s’étirent pas avec l’écran) /// - Bords H / V + centre : s’étirent pour remplir le widget /// - Pas besoin de fichier `.9.png` Android class NineSliceAsset extends StatefulWidget { final String assetPath; /// Zone centrale extensible en pixels source (LTRB du centre). final Rect centerSlice; /// Largeur logique cible des coins gauche / droite. final double cornerWidth; /// Hauteur logique cible des coins haut / bas. final double cornerHeight; final Widget? child; final EdgeInsetsGeometry? padding; final BoxConstraints? constraints; const NineSliceAsset({ super.key, required this.assetPath, required this.centerSlice, this.cornerWidth = 56, this.cornerHeight = 40, this.child, this.padding, this.constraints, }); @override State createState() => _NineSliceAssetState(); } 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(); } } @override void dispose() { _image?.dispose(); super.dispose(); } Future _load() async { final path = widget.assetPath; final data = await rootBundle.load(path); final bytes = data.buffer.asUint8List(); final codec = await ui.instantiateImageCodec(bytes); final frame = await codec.getNextFrame(); if (!mounted) { frame.image.dispose(); return; } setState(() { _image?.dispose(); _image = frame.image; _loadedPath = path; }); } @override Widget build(BuildContext context) { final img = _image; final ready = img != null && _loadedPath == widget.assetPath; return Container( constraints: widget.constraints, child: CustomPaint( painter: ready ? _NineSlicePainter( image: img!, centerSlice: widget.centerSlice, cornerWidth: widget.cornerWidth, cornerHeight: widget.cornerHeight, ) : null, child: Padding( padding: widget.padding ?? EdgeInsets.zero, child: widget.child, ), ), ); } } class _NineSlicePainter extends CustomPainter { final ui.Image image; final Rect centerSlice; final double cornerWidth; final double cornerHeight; _NineSlicePainter({ required this.image, required this.centerSlice, required this.cornerWidth, required this.cornerHeight, }); @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(); // Découpe source (pixels image) — coins / bords / centre. 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); // Destination : coins à taille logique fixe, plafonnés à la moitié // pour rester valides sur très petits widgets. final dstLeftW = cornerWidth.clamp(1.0, size.width / 2); final dstRightW = cornerWidth.clamp(1.0, size.width / 2); final dstTopH = cornerHeight.clamp(1.0, size.height / 2); final dstBottomH = 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); } // Ligne du 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), ); // Ligne du 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), ); // Ligne du 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.centerSlice != centerSlice || oldDelegate.cornerWidth != cornerWidth || oldDelegate.cornerHeight != cornerHeight; } }