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 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) {
|
static Stream<ImageOpEvent> delete(Iterable<ImageEntry> entries) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -7,10 +7,13 @@ import 'package:tuple/tuple.dart';
|
||||||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||||
|
|
||||||
class ServicePolicy {
|
class ServicePolicy {
|
||||||
|
final StreamController<QueueState> _queueStreamController = StreamController<QueueState>.broadcast();
|
||||||
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
||||||
final SplayTreeMap<int, Queue<_Task>> _queues = SplayTreeMap();
|
final SplayTreeMap<int, Queue<_Task>> _queues = SplayTreeMap();
|
||||||
_Task _running;
|
_Task _running;
|
||||||
|
|
||||||
|
Stream<QueueState> get queueStream => _queueStreamController.stream;
|
||||||
|
|
||||||
ServicePolicy._private();
|
ServicePolicy._private();
|
||||||
|
|
||||||
Future<T> call<T>(
|
Future<T> call<T>(
|
||||||
|
@ -60,6 +63,7 @@ class ServicePolicy {
|
||||||
Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>());
|
Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>());
|
||||||
|
|
||||||
void _pickNext() {
|
void _pickNext() {
|
||||||
|
_notifyQueueState();
|
||||||
if (_running != null) return;
|
if (_running != null) return;
|
||||||
final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value;
|
final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value;
|
||||||
_running = queue?.removeFirst();
|
_running = queue?.removeFirst();
|
||||||
|
@ -90,6 +94,13 @@ class ServicePolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPaused(Object key) => _paused.containsKey(key);
|
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 {
|
class _Task {
|
||||||
|
@ -110,3 +121,9 @@ class ServiceCallPriority {
|
||||||
static const int getMetadata = 1000;
|
static const int getMetadata = 1000;
|
||||||
static const int getLocation = 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();
|
_pauseProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isSupported => entry.canDecode;
|
|
||||||
|
|
||||||
void _initProvider() {
|
void _initProvider() {
|
||||||
if (!entry.canDecode) return;
|
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 {
|
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
|
||||||
var uri = key.uri;
|
final uri = key.uri;
|
||||||
var mimeType = key.mimeType;
|
final mimeType = key.mimeType;
|
||||||
try {
|
try {
|
||||||
final bytes = await ImageFileService.getThumbnail(
|
final bytes = await ImageFileService.getThumbnail(
|
||||||
uri,
|
uri,
|
||||||
|
@ -55,7 +55,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
|
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
|
||||||
ImageFileService.resumeThumbnail(key);
|
ImageFileService.resumeLoading(key);
|
||||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +105,15 @@ class ThumbnailProviderKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => hashValues(uri, mimeType, dateModifiedSecs, rotationDegrees, isFlipped, extent, scale);
|
int get hashCode => hashValues(
|
||||||
|
uri,
|
||||||
|
mimeType,
|
||||||
|
dateModifiedSecs,
|
||||||
|
rotationDegrees,
|
||||||
|
isFlipped,
|
||||||
|
extent,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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/cache.dart';
|
||||||
import 'package:aves/widgets/debug/database.dart';
|
import 'package:aves/widgets/debug/database.dart';
|
||||||
import 'package:aves/widgets/debug/firebase.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/settings.dart';
|
||||||
import 'package:aves/widgets/debug/storage.dart';
|
import 'package:aves/widgets/debug/storage.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||||
|
@ -26,6 +27,8 @@ class AppDebugPage extends StatefulWidget {
|
||||||
class AppDebugPageState extends State<AppDebugPage> {
|
class AppDebugPageState extends State<AppDebugPage> {
|
||||||
List<ImageEntry> get entries => widget.source.rawEntries;
|
List<ImageEntry> get entries => widget.source.rawEntries;
|
||||||
|
|
||||||
|
static OverlayEntry _taskQueueOverlayEntry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
|
@ -70,6 +73,22 @@ class AppDebugPageState extends State<AppDebugPage> {
|
||||||
divisions: 9,
|
divisions: 9,
|
||||||
label: '$timeDilation',
|
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(),
|
Divider(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
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/model/image_entry.dart';
|
||||||
import 'package:aves/utils/math_utils.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:aves/widgets/fullscreen/image_view.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -99,7 +99,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
);
|
);
|
||||||
final viewRect = (viewOrigin & viewportSize).inflate(preFetchMargin);
|
final viewRect = (viewOrigin & viewportSize).inflate(preFetchMargin);
|
||||||
|
|
||||||
final tiles = <Widget>[];
|
final tiles = <RegionTile>[];
|
||||||
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
||||||
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
|
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
|
||||||
final layerRegionSize = Size.square(_tileSide * sampleSize);
|
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;
|
final ImageEntry entry;
|
||||||
|
|
||||||
// `tileRect` uses Flutter view coordinates
|
// `tileRect` uses Flutter view coordinates
|
||||||
|
@ -174,35 +174,67 @@ class RegionTile extends StatelessWidget {
|
||||||
@required this.sampleSize,
|
@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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final tileRect = widget.tileRect;
|
||||||
|
|
||||||
Widget child = Image(
|
Widget child = Image(
|
||||||
image: UriRegion(
|
image: _provider,
|
||||||
uri: entry.uri,
|
|
||||||
mimeType: entry.mimeType,
|
|
||||||
rotationDegrees: entry.rotationDegrees,
|
|
||||||
isFlipped: entry.isFlipped,
|
|
||||||
sampleSize: sampleSize,
|
|
||||||
rect: regionRect,
|
|
||||||
),
|
|
||||||
width: tileRect.width,
|
width: tileRect.width,
|
||||||
height: tileRect.height,
|
height: tileRect.height,
|
||||||
fit: BoxFit.fill,
|
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
|
// apply EXIF orientation
|
||||||
final quarterTurns = entry.rotationDegrees ~/ 90;
|
final quarterTurns = entry.rotationDegrees ~/ 90;
|
||||||
if (entry.isFlipped) {
|
if (entry.isFlipped) {
|
||||||
|
@ -230,4 +262,12 @@ class RegionTile extends StatelessWidget {
|
||||||
child: child,
|
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)
|
# - does not support AC3 (by default, but possible by custom build)
|
||||||
# - can play if only the video or audio stream is supported
|
# - can play if only the video or audio stream is supported
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.7.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
Loading…
Reference in a new issue