import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; class SCNinePatchImage extends StatefulWidget { const SCNinePatchImage({ super.key, required this.url, required this.width, required this.height, this.centerSliceRatio = const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34), this.sourceEdgeInset = const EdgeInsets.all(2), this.filterQuality = FilterQuality.low, this.fallback, }); final String url; final double width; final double height; final EdgeInsets centerSliceRatio; final EdgeInsets sourceEdgeInset; final FilterQuality filterQuality; final Widget? fallback; @override State createState() => _SCNinePatchImageState(); } class _SCNinePatchImageState extends State { ImageStream? _imageStream; ImageStreamListener? _imageListener; ui.Image? _image; bool _hasError = false; int _resolveSerial = 0; @override void initState() { super.initState(); _resolveImage(); } @override void didUpdateWidget(covariant SCNinePatchImage oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.url != widget.url || oldWidget.width != widget.width || oldWidget.height != widget.height || oldWidget.sourceEdgeInset != widget.sourceEdgeInset) { _resolveImage(); } } @override void dispose() { _resolveSerial++; _removeListener(); _image?.dispose(); super.dispose(); } void _resolveImage() { _removeListener(); _image?.dispose(); _image = null; final serial = ++_resolveSerial; final url = widget.url.trim(); if (url.isEmpty) { setState(() { _hasError = true; }); return; } final provider = buildCachedImageProvider( url, logicalWidth: widget.width, logicalHeight: widget.height, ); final stream = provider.resolve(ImageConfiguration.empty); late final ImageStreamListener listener; listener = ImageStreamListener( (imageInfo, synchronousCall) { unawaited(_cropAndUseImage(imageInfo.image, serial)); }, onError: (error, stackTrace) { if (!mounted || serial != _resolveSerial) { return; } debugPrint("[SCNinePatchImage] load failed url=$url error=$error"); setState(() { _image = null; _hasError = true; }); }, ); _imageStream = stream; _imageListener = listener; stream.addListener(listener); } Future _cropAndUseImage(ui.Image sourceImage, int serial) async { try { final croppedImage = await _cropImage(sourceImage); if (!mounted || serial != _resolveSerial) { croppedImage.dispose(); return; } setState(() { _image?.dispose(); _image = croppedImage; _hasError = false; }); } catch (error) { debugPrint("[SCNinePatchImage] crop failed error=$error"); if (!mounted || serial != _resolveSerial) { return; } setState(() { _image = null; _hasError = true; }); } } Future _cropImage(ui.Image sourceImage) async { final imageWidth = sourceImage.width.toDouble(); final imageHeight = sourceImage.height.toDouble(); final left = widget.sourceEdgeInset.left.clamp(0.0, imageWidth / 4); final top = widget.sourceEdgeInset.top.clamp(0.0, imageHeight / 4); final right = widget.sourceEdgeInset.right.clamp(0.0, imageWidth / 4); final bottom = widget.sourceEdgeInset.bottom.clamp(0.0, imageHeight / 4); final sourceRect = Rect.fromLTRB( left, top, imageWidth - right, imageHeight - bottom, ); final croppedWidth = sourceRect.width.round().clamp(1, sourceImage.width); final croppedHeight = sourceRect.height.round().clamp( 1, sourceImage.height, ); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawImageRect( sourceImage, sourceRect, Rect.fromLTWH(0, 0, croppedWidth.toDouble(), croppedHeight.toDouble()), Paint()..filterQuality = FilterQuality.low, ); final picture = recorder.endRecording(); final croppedImage = await picture.toImage(croppedWidth, croppedHeight); picture.dispose(); return croppedImage; } void _removeListener() { final stream = _imageStream; final listener = _imageListener; if (stream != null && listener != null) { stream.removeListener(listener); } _imageStream = null; _imageListener = null; } @override Widget build(BuildContext context) { final image = _image; if (_hasError || image == null) { return widget.fallback ?? SizedBox(width: widget.width, height: widget.height); } return RawImage( image: image, width: widget.width, height: widget.height, fit: BoxFit.fill, centerSlice: _buildCenterSlice(image), filterQuality: widget.filterQuality, ); } Rect _buildCenterSlice(ui.Image image) { final imageWidth = image.width.toDouble(); final imageHeight = image.height.toDouble(); final left = (imageWidth * widget.centerSliceRatio.left).clamp( 1.0, imageWidth - 2.0, ); final top = (imageHeight * widget.centerSliceRatio.top).clamp( 1.0, imageHeight - 2.0, ); final right = (imageWidth * widget.centerSliceRatio.right).clamp( 1.0, imageWidth - left - 1.0, ); final bottom = (imageHeight * widget.centerSliceRatio.bottom).clamp( 1.0, imageHeight - top - 1.0, ); return Rect.fromLTRB(left, top, imageWidth - right, imageHeight - bottom); } }