yumi-flutter/lib/ui_kit/widgets/sc_nine_patch_image.dart
2026-04-30 19:54:24 +08:00

210 lines
5.7 KiB
Dart

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<SCNinePatchImage> createState() => _SCNinePatchImageState();
}
class _SCNinePatchImageState extends State<SCNinePatchImage> {
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<void> _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<ui.Image> _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);
}
}