tiling: task management
debug: task queue overlay
This commit is contained in:
parent
ac4f6d344e
commit
b86faea060
10 changed files with 281 additions and 112 deletions
|
@ -193,9 +193,11 @@ class ImageFileService {
|
|||
}
|
||||
}
|
||||
|
||||
static bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]);
|
||||
|
||||
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]);
|
||||
|
||||
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
||||
static Future<T> resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
||||
|
||||
static Stream<ImageOpEvent> delete(Iterable<ImageEntry> entries) {
|
||||
try {
|
||||
|
|
|
@ -7,10 +7,13 @@ import 'package:tuple/tuple.dart';
|
|||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||
|
||||
class ServicePolicy {
|
||||
final StreamController<QueueState> _queueStreamController = StreamController<QueueState>.broadcast();
|
||||
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
||||
final SplayTreeMap<int, Queue<_Task>> _queues = SplayTreeMap();
|
||||
_Task _running;
|
||||
|
||||
Stream<QueueState> get queueStream => _queueStreamController.stream;
|
||||
|
||||
ServicePolicy._private();
|
||||
|
||||
Future<T> call<T>(
|
||||
|
@ -60,6 +63,7 @@ class ServicePolicy {
|
|||
Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>());
|
||||
|
||||
void _pickNext() {
|
||||
_notifyQueueState();
|
||||
if (_running != null) return;
|
||||
final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value;
|
||||
_running = queue?.removeFirst();
|
||||
|
@ -90,6 +94,13 @@ class ServicePolicy {
|
|||
}
|
||||
|
||||
bool isPaused(Object key) => _paused.containsKey(key);
|
||||
|
||||
void _notifyQueueState() {
|
||||
if (!_queueStreamController.hasListener) return;
|
||||
|
||||
final queueByPriority = Map.fromEntries(_queues.entries.map((kv) => MapEntry(kv.key, kv.value.length)));
|
||||
_queueStreamController.add(QueueState(queueByPriority));
|
||||
}
|
||||
}
|
||||
|
||||
class _Task {
|
||||
|
@ -110,3 +121,9 @@ class ServiceCallPriority {
|
|||
static const int getMetadata = 1000;
|
||||
static const int getLocation = 1000;
|
||||
}
|
||||
|
||||
class QueueState {
|
||||
final Map<int, int> queueByPriority;
|
||||
|
||||
const QueueState(this.queueByPriority);
|
||||
}
|
||||
|
|
|
@ -71,8 +71,6 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
|||
_pauseProvider();
|
||||
}
|
||||
|
||||
bool get isSupported => entry.canDecode;
|
||||
|
||||
void _initProvider() {
|
||||
if (!entry.canDecode) return;
|
||||
|
||||
|
|
124
lib/widgets/common/image_providers/region_provider.dart
Normal file
124
lib/widgets/common/image_providers/region_provider.dart
Normal file
|
@ -0,0 +1,124 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||
final RegionProviderKey key;
|
||||
|
||||
RegionProvider(this.key) : assert(key != null);
|
||||
|
||||
@override
|
||||
Future<RegionProviderKey> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<RegionProviderKey>(key);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(RegionProviderKey key, DecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription('uri=${key.uri}, rect=${key.rect}');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderCallback decode) async {
|
||||
final uri = key.uri;
|
||||
final mimeType = key.mimeType;
|
||||
try {
|
||||
final bytes = await ImageFileService.getRegion(
|
||||
uri,
|
||||
mimeType,
|
||||
key.rotationDegrees,
|
||||
key.isFlipped,
|
||||
key.sampleSize,
|
||||
key.rect,
|
||||
taskKey: key,
|
||||
);
|
||||
if (bytes == null) {
|
||||
throw StateError('$uri ($mimeType) region loading failed');
|
||||
}
|
||||
return await decode(bytes);
|
||||
} catch (error) {
|
||||
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
|
||||
throw StateError('$mimeType region decoding failed');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
|
||||
ImageFileService.resumeLoading(key);
|
||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||
}
|
||||
|
||||
void pause() => ImageFileService.cancelRegion(key);
|
||||
}
|
||||
|
||||
class RegionProviderKey {
|
||||
final String uri, mimeType;
|
||||
final int rotationDegrees, sampleSize;
|
||||
final bool isFlipped;
|
||||
final Rect rect;
|
||||
final double scale;
|
||||
|
||||
const RegionProviderKey({
|
||||
@required this.uri,
|
||||
@required this.mimeType,
|
||||
@required this.rotationDegrees,
|
||||
@required this.isFlipped,
|
||||
@required this.sampleSize,
|
||||
@required this.rect,
|
||||
this.scale = 1.0,
|
||||
}) : assert(uri != null),
|
||||
assert(mimeType != null),
|
||||
assert(rotationDegrees != null),
|
||||
assert(isFlipped != null),
|
||||
assert(sampleSize != null),
|
||||
assert(rect != null),
|
||||
assert(scale != null);
|
||||
|
||||
// do not store the entry as it is, because the key should be constant
|
||||
// but the entry attributes may change over time
|
||||
factory RegionProviderKey.fromEntry(
|
||||
ImageEntry entry, {
|
||||
@required int sampleSize,
|
||||
@required Rect rect,
|
||||
}) {
|
||||
return RegionProviderKey(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
sampleSize: sampleSize,
|
||||
rect: rect,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.rect == rect && other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(
|
||||
uri,
|
||||
mimeType,
|
||||
rotationDegrees,
|
||||
isFlipped,
|
||||
mimeType,
|
||||
sampleSize,
|
||||
rect,
|
||||
scale,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RegionProviderKey(uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, rect=$rect, scale=$scale)';
|
||||
}
|
||||
}
|
|
@ -30,8 +30,8 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
|
||||
var uri = key.uri;
|
||||
var mimeType = key.mimeType;
|
||||
final uri = key.uri;
|
||||
final mimeType = key.mimeType;
|
||||
try {
|
||||
final bytes = await ImageFileService.getThumbnail(
|
||||
uri,
|
||||
|
@ -55,7 +55,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
|
||||
@override
|
||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
|
||||
ImageFileService.resumeThumbnail(key);
|
||||
ImageFileService.resumeLoading(key);
|
||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,15 @@ class ThumbnailProviderKey {
|
|||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(uri, mimeType, dateModifiedSecs, rotationDegrees, isFlipped, extent, scale);
|
||||
int get hashCode => hashValues(
|
||||
uri,
|
||||
mimeType,
|
||||
dateModifiedSecs,
|
||||
rotationDegrees,
|
||||
isFlipped,
|
||||
extent,
|
||||
scale,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
class UriRegion extends ImageProvider<UriRegion> {
|
||||
const UriRegion({
|
||||
@required this.uri,
|
||||
@required this.mimeType,
|
||||
@required this.rotationDegrees,
|
||||
@required this.isFlipped,
|
||||
@required this.sampleSize,
|
||||
@required this.rect,
|
||||
this.scale = 1.0,
|
||||
}) : assert(uri != null),
|
||||
assert(scale != null);
|
||||
|
||||
final String uri, mimeType;
|
||||
final int rotationDegrees, sampleSize;
|
||||
final bool isFlipped;
|
||||
final Rect rect;
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Future<UriRegion> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<UriRegion>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(UriRegion key, DecoderCallback decode) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode, chunkEvents),
|
||||
scale: key.scale,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription('uri=$uri, mimeType=$mimeType');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(UriRegion key, DecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
|
||||
assert(key == this);
|
||||
|
||||
try {
|
||||
final bytes = await ImageFileService.getRegion(
|
||||
uri,
|
||||
mimeType,
|
||||
rotationDegrees,
|
||||
isFlipped,
|
||||
sampleSize,
|
||||
rect,
|
||||
);
|
||||
if (bytes == null) {
|
||||
throw StateError('$uri ($mimeType) loading failed');
|
||||
}
|
||||
return await decode(bytes);
|
||||
} catch (error) {
|
||||
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
|
||||
throw StateError('$mimeType decoding failed');
|
||||
} finally {
|
||||
unawaited(chunkEvents.close());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is UriRegion && other.uri == uri && other.sampleSize == sampleSize && other.rect == rect && other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(uri, sampleSize, rect, scale);
|
||||
|
||||
@override
|
||||
String toString() => '${objectRuntimeType(this, 'UriRegion')}(uri=$uri, mimeType=$mimeType, scale=$scale)';
|
||||
}
|
|
@ -6,6 +6,7 @@ import 'package:aves/widgets/debug/android_env.dart';
|
|||
import 'package:aves/widgets/debug/cache.dart';
|
||||
import 'package:aves/widgets/debug/database.dart';
|
||||
import 'package:aves/widgets/debug/firebase.dart';
|
||||
import 'package:aves/widgets/debug/overlay.dart';
|
||||
import 'package:aves/widgets/debug/settings.dart';
|
||||
import 'package:aves/widgets/debug/storage.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||
|
@ -26,6 +27,8 @@ class AppDebugPage extends StatefulWidget {
|
|||
class AppDebugPageState extends State<AppDebugPage> {
|
||||
List<ImageEntry> get entries => widget.source.rawEntries;
|
||||
|
||||
static OverlayEntry _taskQueueOverlayEntry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
|
@ -70,6 +73,22 @@ class AppDebugPageState extends State<AppDebugPage> {
|
|||
divisions: 9,
|
||||
label: '$timeDilation',
|
||||
),
|
||||
SwitchListTile(
|
||||
value: _taskQueueOverlayEntry != null,
|
||||
onChanged: (v) {
|
||||
_taskQueueOverlayEntry?.remove();
|
||||
if (v) {
|
||||
_taskQueueOverlayEntry = OverlayEntry(
|
||||
builder: (context) => DebugTaskQueueOverlay(),
|
||||
);
|
||||
Overlay.of(context).insert(_taskQueueOverlayEntry);
|
||||
} else {
|
||||
_taskQueueOverlayEntry = null;
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
title: Text('Show tasks overlay'),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
|
|
39
lib/widgets/debug/overlay.dart
Normal file
39
lib/widgets/debug/overlay.dart
Normal file
|
@ -0,0 +1,39 @@
|
|||
import 'package:aves/services/service_policy.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DebugTaskQueueOverlay extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
color: Colors.indigo[900].withAlpha(0xCC),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
padding: EdgeInsets.all(8),
|
||||
child: StreamBuilder<QueueState>(
|
||||
stream: servicePolicy.queueStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return SizedBox.shrink();
|
||||
final queuedEntries = (snapshot.hasData ? snapshot.data.queueByPriority.entries.toList() : []);
|
||||
queuedEntries.sort((a, b) => a.key.compareTo(b.key));
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(queuedEntries.map((kv) => '${kv.key}: ${kv.value}').join(', ')),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import 'dart:math';
|
|||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/utils/math_utils.dart';
|
||||
import 'package:aves/widgets/common/image_providers/uri_region_provider.dart';
|
||||
import 'package:aves/widgets/common/image_providers/region_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -99,7 +99,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
);
|
||||
final viewRect = (viewOrigin & viewportSize).inflate(preFetchMargin);
|
||||
|
||||
final tiles = <Widget>[];
|
||||
final tiles = <RegionTile>[];
|
||||
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
||||
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
|
||||
final layerRegionSize = Size.square(_tileSide * sampleSize);
|
||||
|
@ -159,7 +159,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
}
|
||||
}
|
||||
|
||||
class RegionTile extends StatelessWidget {
|
||||
class RegionTile extends StatefulWidget {
|
||||
final ImageEntry entry;
|
||||
|
||||
// `tileRect` uses Flutter view coordinates
|
||||
|
@ -174,35 +174,67 @@ class RegionTile extends StatelessWidget {
|
|||
@required this.sampleSize,
|
||||
});
|
||||
|
||||
@override
|
||||
_RegionTileState createState() => _RegionTileState();
|
||||
}
|
||||
|
||||
class _RegionTileState extends State<RegionTile> {
|
||||
RegionProvider _provider;
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(RegionTile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.entry != widget.entry || oldWidget.tileRect != widget.tileRect || oldWidget.sampleSize != widget.sampleSize || oldWidget.sampleSize != widget.sampleSize) {
|
||||
_unregisterWidget(oldWidget);
|
||||
_registerWidget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(RegionTile widget) {
|
||||
_initProvider();
|
||||
}
|
||||
|
||||
void _unregisterWidget(RegionTile widget) {
|
||||
_pauseProvider();
|
||||
}
|
||||
|
||||
void _initProvider() {
|
||||
if (!entry.canDecode) return;
|
||||
|
||||
_provider = RegionProvider(RegionProviderKey.fromEntry(
|
||||
entry,
|
||||
sampleSize: widget.sampleSize,
|
||||
rect: widget.regionRect,
|
||||
));
|
||||
}
|
||||
|
||||
void _pauseProvider() => _provider?.pause();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tileRect = widget.tileRect;
|
||||
|
||||
Widget child = Image(
|
||||
image: UriRegion(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
sampleSize: sampleSize,
|
||||
rect: regionRect,
|
||||
),
|
||||
image: _provider,
|
||||
width: tileRect.width,
|
||||
height: tileRect.height,
|
||||
fit: BoxFit.fill,
|
||||
// TODO TLAD remove when done with tiling
|
||||
// color: Color.fromARGB((0xff / sampleSize).floor(), 0, 0, 0xff),
|
||||
// colorBlendMode: BlendMode.color,
|
||||
);
|
||||
|
||||
// child = Container(
|
||||
// foregroundDecoration: BoxDecoration(
|
||||
// border: Border.all(
|
||||
// color: Colors.cyan,
|
||||
// ),
|
||||
// ),
|
||||
// // child: Text('$sampleSize'),
|
||||
// child: child,
|
||||
// );
|
||||
|
||||
// apply EXIF orientation
|
||||
final quarterTurns = entry.rotationDegrees ~/ 90;
|
||||
if (entry.isFlipped) {
|
||||
|
@ -230,4 +262,12 @@ class RegionTile extends StatelessWidget {
|
|||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('contentId', widget.entry.contentId));
|
||||
properties.add(IntProperty('sampleSize', widget.sampleSize));
|
||||
properties.add(DiagnosticsProperty<Rect>('regionRect', widget.regionRect));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,9 @@ version: 1.2.5+31
|
|||
# - does not support AC3 (by default, but possible by custom build)
|
||||
# - can play if only the video or audio stream is supported
|
||||
|
||||
environment:
|
||||
sdk: ">=2.7.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
|
Loading…
Reference in a new issue