Files
petitspas/frontend/lib/widgets/quotidien/nine_slice_asset.dart
T
jmartinandCursor 42163826fe fix(#173): 9-slice manuel avec coins à taille logique fixe
Remplace DecorationImage.centerSlice (inopérant si bulle trop
basse) par un peintre 9 zones : coins fixes, centre extensible.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 17:10:40 +02:00

215 lines
6.0 KiB
Dart

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<NineSliceAsset> createState() => _NineSliceAssetState();
}
class _NineSliceAssetState extends State<NineSliceAsset> {
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<void> _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;
}
}