您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
515 行
17 KiB
515 行
17 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Unity.UIWidgets.async2;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.scheduler2;
|
|
using Unity.UIWidgets.ui;
|
|
using SchedulerBinding = Unity.UIWidgets.scheduler2.SchedulerBinding;
|
|
|
|
namespace Unity.UIWidgets.painting {
|
|
public class ImageInfo : IEquatable<ImageInfo> {
|
|
public ImageInfo(Image image, float scale = 1.0f) {
|
|
D.assert(image != null);
|
|
|
|
this.image = image;
|
|
this.scale = scale;
|
|
}
|
|
|
|
public readonly Image image;
|
|
public readonly float scale;
|
|
|
|
public bool Equals(ImageInfo other) {
|
|
if (ReferenceEquals(null, other)) {
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, other)) {
|
|
return true;
|
|
}
|
|
|
|
return image.Equals(other.image) && scale.Equals(other.scale);
|
|
}
|
|
|
|
public override bool Equals(object obj) {
|
|
if (ReferenceEquals(null, obj)) {
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, obj)) {
|
|
return true;
|
|
}
|
|
|
|
if (obj.GetType() != GetType()) {
|
|
return false;
|
|
}
|
|
|
|
return Equals((ImageInfo) obj);
|
|
}
|
|
|
|
public override int GetHashCode() {
|
|
unchecked {
|
|
return ((image != null ? image.GetHashCode() : 0) * 397) ^ scale.GetHashCode();
|
|
}
|
|
}
|
|
|
|
public static bool operator ==(ImageInfo left, ImageInfo right) {
|
|
return Equals(left, right);
|
|
}
|
|
|
|
public static bool operator !=(ImageInfo left, ImageInfo right) {
|
|
return !Equals(left, right);
|
|
}
|
|
|
|
public override string ToString() {
|
|
return $"{image} @ {scale}x";
|
|
}
|
|
}
|
|
|
|
|
|
public class ImageStreamListener : IEquatable<ImageStreamListener> {
|
|
public ImageStreamListener(
|
|
ImageListener onImage,
|
|
ImageChunkListener onChunk = null,
|
|
ImageErrorListener onError = null
|
|
) {
|
|
D.assert(onImage != null);
|
|
this.onImage = onImage;
|
|
this.onChunk = onChunk;
|
|
this.onError = onError;
|
|
}
|
|
|
|
public readonly ImageListener onImage;
|
|
|
|
public readonly ImageChunkListener onChunk;
|
|
|
|
public readonly ImageErrorListener onError;
|
|
|
|
public bool Equals(ImageStreamListener other) {
|
|
if (ReferenceEquals(null, other)) {
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, other)) {
|
|
return true;
|
|
}
|
|
|
|
return Equals(onImage, other.onImage) && Equals(onChunk, other.onChunk) && Equals(onError, other.onError);
|
|
}
|
|
|
|
public override bool Equals(object obj) {
|
|
if (ReferenceEquals(null, obj)) {
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, obj)) {
|
|
return true;
|
|
}
|
|
|
|
if (obj.GetType() != GetType()) {
|
|
return false;
|
|
}
|
|
|
|
return Equals((ImageStreamListener) obj);
|
|
}
|
|
|
|
public static bool operator ==(ImageStreamListener left, ImageStreamListener right) {
|
|
return Equals(left, right);
|
|
}
|
|
|
|
public static bool operator !=(ImageStreamListener left, ImageStreamListener right) {
|
|
return !Equals(left, right);
|
|
}
|
|
|
|
public override int GetHashCode() {
|
|
unchecked {
|
|
var hashCode = (onImage != null ? onImage.GetHashCode() : 0);
|
|
hashCode = (hashCode * 397) ^ (onChunk != null ? onChunk.GetHashCode() : 0);
|
|
hashCode = (hashCode * 397) ^ (onError != null ? onError.GetHashCode() : 0);
|
|
return hashCode;
|
|
}
|
|
}
|
|
}
|
|
|
|
public delegate void ImageListener(ImageInfo image, bool synchronousCall);
|
|
|
|
public delegate void ImageChunkListener(ImageChunkEvent evt);
|
|
|
|
public delegate void ImageErrorListener(Exception exception);
|
|
|
|
public class ImageChunkEvent : Diagnosticable {
|
|
public ImageChunkEvent(
|
|
int cumulativeBytesLoaded,
|
|
int expectedTotalBytes
|
|
) {
|
|
D.assert(cumulativeBytesLoaded >= 0);
|
|
D.assert(expectedTotalBytes >= 0);
|
|
this.cumulativeBytesLoaded = cumulativeBytesLoaded;
|
|
this.expectedTotalBytes = expectedTotalBytes;
|
|
}
|
|
|
|
public readonly int cumulativeBytesLoaded;
|
|
|
|
public readonly int expectedTotalBytes;
|
|
|
|
public override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
base.debugFillProperties(properties);
|
|
properties.add(new IntProperty("cumulativeBytesLoaded", cumulativeBytesLoaded));
|
|
properties.add(new IntProperty("expectedTotalBytes", expectedTotalBytes));
|
|
}
|
|
}
|
|
|
|
public class ImageStream : Diagnosticable {
|
|
public ImageStream() {
|
|
}
|
|
|
|
ImageStreamCompleter _completer;
|
|
|
|
public ImageStreamCompleter completer {
|
|
get { return _completer; }
|
|
}
|
|
|
|
List<ImageStreamListener> _listeners;
|
|
|
|
public void setCompleter(ImageStreamCompleter value) {
|
|
D.assert(_completer == null);
|
|
|
|
_completer = value;
|
|
if (_listeners != null) {
|
|
var initialListeners = _listeners;
|
|
_listeners = null;
|
|
initialListeners.ForEach(_completer.addListener);
|
|
}
|
|
}
|
|
|
|
public void addListener(ImageStreamListener listener) {
|
|
if (_completer != null) {
|
|
_completer.addListener(listener);
|
|
return;
|
|
}
|
|
|
|
if (_listeners == null) {
|
|
_listeners = new List<ImageStreamListener>();
|
|
}
|
|
|
|
_listeners.Add(listener);
|
|
}
|
|
|
|
public void removeListener(ImageStreamListener listener) {
|
|
if (_completer != null) {
|
|
_completer.removeListener(listener);
|
|
return;
|
|
}
|
|
|
|
D.assert(_listeners != null);
|
|
for (int i = 0; i < _listeners.Count; i++) {
|
|
if (_listeners[i] == listener) {
|
|
_listeners.RemoveAt(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public object key {
|
|
get { return _completer != null ? (object) _completer : this; }
|
|
}
|
|
|
|
public override void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
base.debugFillProperties(properties);
|
|
properties.add(new ObjectFlagProperty<ImageStreamCompleter>(
|
|
"completer",
|
|
_completer,
|
|
ifPresent: _completer?.toStringShort(),
|
|
ifNull: "unresolved"
|
|
));
|
|
properties.add(new ObjectFlagProperty<List<ImageStreamListener>>(
|
|
"listeners",
|
|
_listeners,
|
|
ifPresent: $"{_listeners?.Count} listener{(_listeners?.Count == 1 ? "" : "s")}",
|
|
ifNull: "no listeners",
|
|
level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info
|
|
));
|
|
_completer?.debugFillProperties(properties);
|
|
}
|
|
}
|
|
|
|
public abstract class ImageStreamCompleter : Diagnosticable {
|
|
internal readonly List<ImageStreamListener> _listeners = new List<ImageStreamListener>();
|
|
internal ImageInfo _currentImage;
|
|
internal UIWidgetsErrorDetails _currentError;
|
|
|
|
protected bool hasListeners {
|
|
get { return _listeners.isNotEmpty(); }
|
|
}
|
|
|
|
public virtual void addListener(ImageStreamListener listener) {
|
|
_listeners.Add(listener);
|
|
if (_currentImage != null) {
|
|
try {
|
|
listener.onImage(_currentImage, true);
|
|
}
|
|
catch (Exception exception) {
|
|
reportError(
|
|
context: new ErrorDescription("by a synchronously-called image listener"),
|
|
exception: exception
|
|
);
|
|
}
|
|
}
|
|
|
|
if (_currentError != null && listener.onError != null) {
|
|
try {
|
|
listener.onError(_currentError.exception);
|
|
}
|
|
catch (Exception exception) {
|
|
UIWidgetsError.reportError(
|
|
new UIWidgetsErrorDetails(
|
|
exception: exception,
|
|
library: "image resource service",
|
|
context: new ErrorDescription("by a synchronously-called image error listener")
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void removeListener(ImageStreamListener listener) {
|
|
for (int i = 0; i < _listeners.Count; i += 1) {
|
|
if (_listeners[i] == listener) {
|
|
_listeners.RemoveAt(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_listeners.isEmpty()) {
|
|
foreach (VoidCallback callback in _onLastListenerRemovedCallbacks) {
|
|
callback();
|
|
}
|
|
|
|
_onLastListenerRemovedCallbacks.Clear();
|
|
}
|
|
}
|
|
|
|
readonly List<VoidCallback> _onLastListenerRemovedCallbacks = new List<VoidCallback>();
|
|
|
|
public void addOnLastListenerRemovedCallback(VoidCallback callback) {
|
|
D.assert(callback != null);
|
|
_onLastListenerRemovedCallbacks.Add(callback);
|
|
}
|
|
|
|
public void removeOnLastListenerRemovedCallback(VoidCallback callback) {
|
|
D.assert(callback != null);
|
|
_onLastListenerRemovedCallbacks.Remove(callback);
|
|
}
|
|
|
|
|
|
protected void setImage(ImageInfo image) {
|
|
_currentImage = image;
|
|
if (_listeners.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
var localListeners = _listeners.Select(l => l).ToList();
|
|
foreach (var listener in localListeners) {
|
|
try {
|
|
listener.onImage(image, false);
|
|
}
|
|
catch (Exception ex) {
|
|
reportError(
|
|
context: new ErrorDescription("by an image listener"),
|
|
exception: ex
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void reportError(
|
|
DiagnosticsNode context = null,
|
|
Exception exception = null,
|
|
InformationCollector informationCollector = null,
|
|
bool silent = false) {
|
|
_currentError = new UIWidgetsErrorDetails(
|
|
exception: exception,
|
|
library: "image resource service",
|
|
context: context,
|
|
informationCollector: informationCollector,
|
|
silent: silent
|
|
);
|
|
|
|
var localErrorListeners = _listeners
|
|
.Select(l => l.onError)
|
|
.Where(l => l != null)
|
|
.ToList();
|
|
|
|
if (localErrorListeners.isEmpty()) {
|
|
UIWidgetsError.reportError(_currentError);
|
|
}
|
|
else {
|
|
foreach (var errorListener in localErrorListeners) {
|
|
try {
|
|
errorListener(exception);
|
|
}
|
|
catch (Exception ex) {
|
|
UIWidgetsError.reportError(
|
|
new UIWidgetsErrorDetails(
|
|
context: new ErrorDescription("when reporting an error to an image listener"),
|
|
library: "image resource service",
|
|
exception: ex
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
|
base.debugFillProperties(description);
|
|
description.add(new DiagnosticsProperty<ImageInfo>(
|
|
"current", _currentImage, ifNull: "unresolved", showName: false));
|
|
description.add(new ObjectFlagProperty<List<ImageStreamListener>>(
|
|
"listeners",
|
|
_listeners,
|
|
ifPresent: $"{_listeners.Count} listener{(_listeners.Count == 1 ? "" : "s")}"
|
|
));
|
|
}
|
|
}
|
|
|
|
public class OneFrameImageStreamCompleter : ImageStreamCompleter {
|
|
public OneFrameImageStreamCompleter(Future<ImageInfo> image,
|
|
InformationCollector informationCollector = null) {
|
|
D.assert(image != null);
|
|
|
|
image.then_(result => { setImage(result); }).catchError(err => {
|
|
reportError(
|
|
context: new ErrorDescription("resolving a single-frame image stream"),
|
|
exception: err,
|
|
informationCollector: informationCollector,
|
|
silent: true
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
// TODO: update stream
|
|
public class MultiFrameImageStreamCompleter : ImageStreamCompleter {
|
|
public MultiFrameImageStreamCompleter(
|
|
Future<Codec> codec,
|
|
float scale,
|
|
InformationCollector informationCollector = null
|
|
) {
|
|
D.assert(codec != null);
|
|
|
|
_scale = scale;
|
|
_informationCollector = informationCollector;
|
|
|
|
codec.then_((Action<Codec>) _handleCodecReady, ex => {
|
|
reportError(
|
|
context: new ErrorDescription("resolving an image codec"),
|
|
exception: ex,
|
|
informationCollector: informationCollector,
|
|
silent: true
|
|
);
|
|
return FutureOr.nil;
|
|
});
|
|
}
|
|
|
|
Codec _codec;
|
|
readonly float _scale;
|
|
readonly InformationCollector _informationCollector;
|
|
|
|
FrameInfo _nextFrame;
|
|
TimeSpan? _shownTimestamp;
|
|
TimeSpan? _frameDuration;
|
|
int _framesEmitted = 0;
|
|
Timer _timer;
|
|
|
|
bool _frameCallbackScheduled = false;
|
|
|
|
void _handleCodecReady(Codec codec) {
|
|
_codec = codec;
|
|
D.assert(_codec != null);
|
|
|
|
if (hasListeners) {
|
|
_decodeNextFrameAndSchedule();
|
|
}
|
|
}
|
|
|
|
void _handleAppFrame(TimeSpan timestamp) {
|
|
_frameCallbackScheduled = false;
|
|
if (!hasListeners) {
|
|
return;
|
|
}
|
|
|
|
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
|
|
_emitFrame(new ImageInfo(image: _nextFrame.image, scale: _scale));
|
|
_shownTimestamp = timestamp;
|
|
_frameDuration = _nextFrame.duration;
|
|
_nextFrame = null;
|
|
int completedCycles = _codec.frameCount == 0 ? 0 : _framesEmitted / _codec.frameCount;
|
|
|
|
if (_codec.repetitionCount == -1 || completedCycles <= _codec.repetitionCount) {
|
|
_decodeNextFrameAndSchedule();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
TimeSpan delay = _frameDuration.Value - (timestamp - _shownTimestamp.Value);
|
|
delay = new TimeSpan((long) (delay.Ticks * scheduler_.timeDilation));
|
|
// TODO: time dilation
|
|
_timer = Timer.create(delay, () => _scheduleAppFrame());
|
|
}
|
|
|
|
bool _isFirstFrame() {
|
|
return _frameDuration == null;
|
|
}
|
|
|
|
bool _hasFrameDurationPassed(TimeSpan timestamp) {
|
|
D.assert(_shownTimestamp != null);
|
|
return timestamp - _shownTimestamp >= _frameDuration;
|
|
}
|
|
|
|
Future _decodeNextFrameAndSchedule() {
|
|
var frame = _codec.getNextFrame();
|
|
return frame.then_(info => {
|
|
_nextFrame = info;
|
|
if (_codec.frameCount == 1) {
|
|
_emitFrame(new ImageInfo(image: _nextFrame.image, scale: _scale));
|
|
return;
|
|
}
|
|
|
|
_scheduleAppFrame();
|
|
});
|
|
}
|
|
|
|
void _scheduleAppFrame() {
|
|
if (_frameCallbackScheduled) {
|
|
return;
|
|
}
|
|
|
|
_frameCallbackScheduled = true;
|
|
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
|
|
}
|
|
|
|
void _emitFrame(ImageInfo imageInfo) {
|
|
setImage(imageInfo);
|
|
_framesEmitted += 1;
|
|
}
|
|
|
|
public override void addListener(ImageStreamListener listener) {
|
|
if (!hasListeners && _codec != null) {
|
|
_decodeNextFrameAndSchedule();
|
|
}
|
|
|
|
base.addListener(listener);
|
|
}
|
|
|
|
public override void removeListener(ImageStreamListener listener) {
|
|
base.removeListener(listener);
|
|
if (!hasListeners) {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
}
|
|
}
|
|
}
|
|
}
|