208 lines
5.5 KiB
Dart
208 lines
5.5 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;
|
|
}
|
|
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) {
|
|
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);
|
|
}
|
|
}
|