thumbnail: cancel queued image loading on dispose

This commit is contained in:
Thibault Deckers 2020-04-16 18:35:33 +09:00
parent 28e053cdd6
commit 19976940a0
18 changed files with 229 additions and 172 deletions

View file

@ -23,7 +23,6 @@ import com.bumptech.glide.signature.ObjectKey;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import deckers.thibault.aves.decoder.VideoThumbnail; import deckers.thibault.aves.decoder.VideoThumbnail;
import deckers.thibault.aves.model.ImageEntry; import deckers.thibault.aves.model.ImageEntry;
@ -37,14 +36,12 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
ImageEntry entry; ImageEntry entry;
int width, height; int width, height;
MethodChannel.Result result; MethodChannel.Result result;
Consumer<String> complete;
Params(ImageEntry entry, int width, int height, MethodChannel.Result result, Consumer<String> complete) { Params(ImageEntry entry, int width, int height, MethodChannel.Result result) {
this.entry = entry; this.entry = entry;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.result = result; this.result = result;
this.complete = complete;
} }
} }
@ -127,11 +124,14 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
} else { } else {
Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null); Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null);
// from Android Q, returned thumbnail is already rotated according to EXIF orientation // from Android Q, returned thumbnail is already rotated according to EXIF orientation
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null && entry.orientationDegrees != 0) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
Integer orientationDegrees = entry.orientationDegrees;
if (orientationDegrees != null && orientationDegrees != 0) {
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
matrix.postRotate(entry.orientationDegrees); matrix.postRotate(orientationDegrees);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
} }
}
return bitmap; return bitmap;
} }
} }
@ -176,7 +176,6 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
protected void onPostExecute(Result result) { protected void onPostExecute(Result result) {
MethodChannel.Result r = result.params.result; MethodChannel.Result r = result.params.result;
String uri = result.params.entry.uri.toString(); String uri = result.params.entry.uri.toString();
result.params.complete.accept(uri);
if (result.data != null) { if (result.data != null) {
r.success(result.data); r.success(result.data);
} else { } else {

View file

@ -1,45 +0,0 @@
package deckers.thibault.aves.channelhandlers;
import android.app.Activity;
import android.util.Log;
import java.util.concurrent.LinkedBlockingDeque;
import deckers.thibault.aves.model.ImageEntry;
import deckers.thibault.aves.utils.Utils;
import io.flutter.plugin.common.MethodChannel;
public class ImageDecodeTaskManager {
private static final String LOG_TAG = Utils.createLogTag(ImageDecodeTaskManager.class);
private LinkedBlockingDeque<ImageDecodeTask.Params> taskParamsQueue;
private boolean running = true;
ImageDecodeTaskManager(Activity activity) {
taskParamsQueue = new LinkedBlockingDeque<>();
new Thread(() -> {
try {
while (running) {
ImageDecodeTask.Params params = taskParamsQueue.take();
new ImageDecodeTask(activity).execute(params);
Thread.sleep(10);
}
} catch (InterruptedException ex) {
Log.w(LOG_TAG, ex);
}
}).start();
}
void fetch(MethodChannel.Result result, ImageEntry entry, Integer width, Integer height) {
taskParamsQueue.addFirst(new ImageDecodeTask.Params(entry, width, height, result, this::complete));
}
void cancel(String uri) {
boolean removed = taskParamsQueue.removeIf(p -> uri.equals(p.entry.uri.toString()));
if (removed) Log.d(LOG_TAG, "cancelled uri=" + uri);
}
private void complete(String uri) {
// nothing for now
}
}

View file

@ -34,12 +34,10 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/image"; public static final String CHANNEL = "deckers.thibault/aves/image";
private Activity activity; private Activity activity;
private ImageDecodeTaskManager imageDecodeTaskManager;
private MediaStoreStreamHandler mediaStoreStreamHandler; private MediaStoreStreamHandler mediaStoreStreamHandler;
public ImageFileHandler(Activity activity, MediaStoreStreamHandler mediaStoreStreamHandler) { public ImageFileHandler(Activity activity, MediaStoreStreamHandler mediaStoreStreamHandler) {
this.activity = activity; this.activity = activity;
imageDecodeTaskManager = new ImageDecodeTaskManager(activity);
this.mediaStoreStreamHandler = mediaStoreStreamHandler; this.mediaStoreStreamHandler = mediaStoreStreamHandler;
} }
@ -59,9 +57,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
case "getThumbnail": case "getThumbnail":
new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start(); new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start();
break; break;
case "cancelGetThumbnail":
new Thread(() -> cancelGetThumbnail(call, new MethodResultWrapper(result))).start();
break;
case "delete": case "delete":
new Thread(() -> delete(call, new MethodResultWrapper(result))).start(); new Thread(() -> delete(call, new MethodResultWrapper(result))).start();
break; break;
@ -145,13 +140,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
return; return;
} }
ImageEntry entry = new ImageEntry(entryMap); ImageEntry entry = new ImageEntry(entryMap);
imageDecodeTaskManager.fetch(result, entry, width, height); new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(entry, width, height, result));
}
private void cancelGetThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String uri = call.argument("uri");
imageDecodeTaskManager.cancel(uri);
result.success(null);
} }
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
@ -275,7 +264,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
int bufferSize = 1024; int bufferSize = 1024;
byte[] buffer = new byte[bufferSize]; byte[] buffer = new byte[bufferSize];
int len = 0; int len;
while ((len = inputStream.read(buffer)) != -1) { while ((len = inputStream.read(buffer)) != -1) {
byteBuffer.write(buffer, 0, len); byteBuffer.write(buffer, 0, len);
} }

View file

@ -223,8 +223,8 @@ class ImageEntry {
try { try {
final addresses = await servicePolicy.call( final addresses = await servicePolicy.call(
() => Geocoder.local.findAddressesFromCoordinates(coordinates), () => Geocoder.local.findAddressesFromCoordinates(coordinates),
ServiceCallPriority.background, priority: ServiceCallPriority.background,
'findAddressesFromCoordinates-$path', debugLabel: 'findAddressesFromCoordinates-$path',
); );
if (addresses != null && addresses.isNotEmpty) { if (addresses != null && addresses.isNotEmpty) {
final address = addresses.first; final address = addresses.first;

View file

@ -43,7 +43,7 @@ class ImageFileService {
return Uint8List(0); return Uint8List(0);
} }
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height) { static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object cancellationKey}) {
return servicePolicy.call( return servicePolicy.call(
() async { () async {
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
@ -61,21 +61,12 @@ class ImageFileService {
} }
return Uint8List(0); return Uint8List(0);
}, },
ServiceCallPriority.asapLifo, priority: ServiceCallPriority.asap,
'getThumbnail-${entry.path}', debugLabel: 'getThumbnail-${entry.path}',
cancellationKey: cancellationKey,
); );
} }
static Future<void> cancelGetThumbnail(String uri) async {
try {
await platform.invokeMethod('cancelGetThumbnail', <String, dynamic>{
'uri': uri,
});
} on PlatformException catch (e) {
debugPrint('cancelGetThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
}
}
static Future<bool> delete(ImageEntry entry) async { static Future<bool> delete(ImageEntry entry) async {
try { try {
await platform.invokeMethod('delete', <String, dynamic>{ await platform.invokeMethod('delete', <String, dynamic>{

View file

@ -50,8 +50,8 @@ class MetadataService {
} }
return null; return null;
}, },
ServiceCallPriority.background, priority: ServiceCallPriority.background,
'getCatalogMetadata-${entry.path}', debugLabel: 'getCatalogMetadata-${entry.path}',
); );
} }

View file

@ -1,47 +1,55 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart';
final ServicePolicy servicePolicy = ServicePolicy._private(); final ServicePolicy servicePolicy = ServicePolicy._private();
class ServicePolicy { class ServicePolicy {
final Queue<VoidCallback> _asapQueue = Queue(), _normalQueue = Queue(), _backgroundQueue = Queue(); final Queue<_Task> _asapQueue, _normalQueue, _backgroundQueue;
VoidCallback _running; List<Queue<_Task>> _queues;
_Task _running;
ServicePolicy._private(); ServicePolicy._private()
: _asapQueue = Queue(),
_normalQueue = Queue(),
_backgroundQueue = Queue() {
_queues = [_asapQueue, _normalQueue, _backgroundQueue];
}
Future<T> call<T>(Future<T> Function() platformCall, [ServiceCallPriority priority = ServiceCallPriority.normal, String debugLabel]) { Future<T> call<T>(
Queue<VoidCallback> q; Future<T> Function() platformCall, {
ServiceCallPriority priority = ServiceCallPriority.normal,
String debugLabel,
Object cancellationKey,
}) {
Queue<_Task> queue;
switch (priority) { switch (priority) {
case ServiceCallPriority.asapFifo: case ServiceCallPriority.asap:
q = _asapQueue; queue = _asapQueue;
break;
case ServiceCallPriority.asapLifo:
q = _asapQueue;
break; break;
case ServiceCallPriority.background: case ServiceCallPriority.background:
q = _backgroundQueue; queue = _backgroundQueue;
break; break;
case ServiceCallPriority.normal: case ServiceCallPriority.normal:
default: default:
q = _normalQueue; queue = _normalQueue;
break; break;
} }
final completer = Completer<T>(); final completer = Completer<T>();
final wrapped = () async { final wrapped = _Task(
() async {
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel start'); // if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
final result = await platformCall(); final result = await platformCall();
completer.complete(result); completer.complete(result);
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed'); // if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
_running = null; _running = null;
_pickNext(); _pickNext();
}; },
if (priority == ServiceCallPriority.asapLifo) { completer,
q.addFirst(wrapped); cancellationKey,
} else { );
q.addLast(wrapped); queue.addLast(wrapped);
}
_pickNext(); _pickNext();
return completer.future; return completer.future;
@ -49,10 +57,32 @@ class ServicePolicy {
void _pickNext() { void _pickNext() {
if (_running != null) return; if (_running != null) return;
final queue = [_asapQueue, _normalQueue, _backgroundQueue].firstWhere((q) => q.isNotEmpty, orElse: () => null); final queue = _queues.firstWhere((q) => q.isNotEmpty, orElse: () => null);
_running = queue?.removeFirst(); _running = queue?.removeFirst();
_running?.call(); _running?.callback?.call();
}
bool cancel(Object cancellationKey) {
var cancelled = false;
final tasks = _queues.expand((q) => q.where((task) => task.cancellationKey == cancellationKey)).toList();
tasks.forEach((task) => _queues.forEach((q) {
if (q.remove(task)) {
cancelled = true;
task.completer.completeError(CancelledException());
}
}));
return cancelled;
} }
} }
enum ServiceCallPriority { asapFifo, asapLifo, normal, background } class _Task {
final VoidCallback callback;
final Completer completer;
final Object cancellationKey;
const _Task(this.callback, this.completer, this.cancellationKey);
}
class CancelledException {}
enum ServiceCallPriority { asap, normal, background }

View file

@ -7,7 +7,6 @@ import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/album/grid/header_album.dart'; import 'package:aves/widgets/album/grid/header_album.dart';
import 'package:aves/widgets/album/grid/header_date.dart'; import 'package:aves/widgets/album/grid/header_date.dart';
import 'package:aves/widgets/common/fx/outlined_text.dart'; import 'package:aves/widgets/common/fx/outlined_text.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';

View file

@ -91,10 +91,12 @@ class SectionedListLayoutProvider extends StatelessWidget {
final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount); final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount);
final children = <Widget>[]; final children = <Widget>[];
for (var i = minEntryIndex; i < maxEntryIndex; i++) { for (var i = minEntryIndex; i < maxEntryIndex; i++) {
final entry = section[i];
children.add(GridThumbnail( children.add(GridThumbnail(
key: ValueKey(entry.contentId),
collection: collection, collection: collection,
index: i, index: i,
entry: section[i], entry: entry,
tileExtent: tileExtent, tileExtent: tileExtent,
)); ));
} }

View file

@ -54,7 +54,7 @@ class GridThumbnail extends StatelessWidget {
onTap: () => _goToFullscreen(context), onTap: () => _goToFullscreen(context),
child: MetaData( child: MetaData(
metaData: ThumbnailMetadata(index, entry), metaData: ThumbnailMetadata(index, entry),
child: Thumbnail( child: DecoratedThumbnail(
entry: entry, entry: entry,
extent: tileExtent, extent: tileExtent,
heroTag: collection.heroTag(entry), heroTag: collection.heroTag(entry),

View file

@ -204,7 +204,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
Positioned( Positioned(
left: clampedCenter.dx - extent / 2, left: clampedCenter.dx - extent / 2,
top: clampedCenter.dy - extent / 2, top: clampedCenter.dy - extent / 2,
child: Thumbnail( child: DecoratedThumbnail(
entry: widget.imageEntry, entry: widget.imageEntry,
extent: extent, extent: extent,
), ),
@ -232,12 +232,12 @@ class GridPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final paint = Paint() final paint = Paint()
..strokeWidth = Thumbnail.borderWidth ..strokeWidth = DecoratedThumbnail.borderWidth
..shader = ui.Gradient.radial( ..shader = ui.Gradient.radial(
center, center,
size.width / 2, size.width / 2,
[ [
Thumbnail.borderColor, DecoratedThumbnail.borderColor,
Colors.transparent, Colors.transparent,
], ],
[ [

View file

@ -10,7 +10,7 @@ import 'package:aves/widgets/common/transition_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
class Thumbnail extends StatelessWidget { class DecoratedThumbnail extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
final double extent; final double extent;
final Object heroTag; final Object heroTag;
@ -18,7 +18,7 @@ class Thumbnail extends StatelessWidget {
static final Color borderColor = Colors.grey.shade700; static final Color borderColor = Colors.grey.shade700;
static const double borderWidth = .5; static const double borderWidth = .5;
const Thumbnail({ const DecoratedThumbnail({
Key key, Key key,
@required this.entry, @required this.entry,
@required this.extent, @required this.extent,
@ -39,7 +39,13 @@ class Thumbnail extends StatelessWidget {
child: Stack( child: Stack(
alignment: AlignmentDirectional.bottomStart, alignment: AlignmentDirectional.bottomStart,
children: [ children: [
entry.isSvg ? _buildVectorImage() : _buildRasterImage(), entry.isSvg
? _buildVectorImage()
: ThumbnailRasterImage(
entry: entry,
extent: extent,
heroTag: heroTag,
),
_ThumbnailOverlay( _ThumbnailOverlay(
entry: entry, entry: entry,
extent: extent, extent: extent,
@ -49,38 +55,6 @@ class Thumbnail extends StatelessWidget {
); );
} }
Widget _buildRasterImage() {
final thumbnailProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent);
final image = Image(
image: thumbnailProvider,
width: extent,
height: extent,
fit: BoxFit.cover,
);
return heroTag == null
? image
: Hero(
tag: heroTag,
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
ImageProvider heroImageProvider = thumbnailProvider;
if (!entry.isVideo && !entry.isSvg) {
final imageProvider = UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
);
if (imageCache.statusForKey(imageProvider).keepAlive) {
heroImageProvider = imageProvider;
}
}
return TransitionImage(
image: heroImageProvider,
animation: animation,
);
},
child: image,
);
}
Widget _buildVectorImage() { Widget _buildVectorImage() {
final child = Container( final child = Container(
// center `SvgPicture` inside `Container` with the thumbnail dimensions // center `SvgPicture` inside `Container` with the thumbnail dimensions
@ -106,6 +80,89 @@ class Thumbnail extends StatelessWidget {
} }
} }
class ThumbnailRasterImage extends StatefulWidget {
final ImageEntry entry;
final double extent;
final Object heroTag;
const ThumbnailRasterImage({
Key key,
@required this.entry,
@required this.extent,
this.heroTag,
}) : super(key: key);
@override
_ThumbnailRasterImageState createState() => _ThumbnailRasterImageState();
}
class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
ThumbnailProvider _imageProvider;
ImageEntry get entry => widget.entry;
double get extent => widget.extent;
Object get heroTag => widget.heroTag;
@override
void initState() {
super.initState();
_initProvider();
}
@override
void didUpdateWidget(ThumbnailRasterImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != entry) {
_cancelProvider();
_initProvider();
}
}
@override
void dispose() {
_cancelProvider();
super.dispose();
}
void _initProvider() => _imageProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent);
void _cancelProvider() => _imageProvider?.cancel();
@override
Widget build(BuildContext context) {
final image = Image(
image: _imageProvider,
width: extent,
height: extent,
fit: BoxFit.cover,
);
return heroTag == null
? image
: Hero(
tag: heroTag,
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
ImageProvider heroImageProvider = _imageProvider;
if (!entry.isVideo && !entry.isSvg) {
final imageProvider = UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
);
if (imageCache.statusForKey(imageProvider).keepAlive) {
heroImageProvider = imageProvider;
}
}
return TransitionImage(
image: heroImageProvider,
animation: animation,
);
},
child: image,
);
}
}
class _ThumbnailOverlay extends StatelessWidget { class _ThumbnailOverlay extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
final double extent; final double extent;

View file

@ -1,3 +1,4 @@
import 'dart:typed_data';
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui show Codec;
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
@ -38,11 +39,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderCallback decode) async { Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderCallback decode) async {
final bytes = await AndroidAppService.getAppIcon(key.packageName, key.sizePixels); final bytes = await AndroidAppService.getAppIcon(key.packageName, key.sizePixels);
if (bytes.lengthInBytes == 0) { return await decode(bytes ?? Uint8List(0));
return null;
}
return await decode(bytes);
} }
} }

View file

@ -1,12 +1,15 @@
import 'dart:typed_data';
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui show Codec;
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/service_policy.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> { class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
const ThumbnailProvider({ ThumbnailProvider({
@required this.entry, @required this.entry,
@required this.extent, @required this.extent,
this.scale = 1.0, this.scale = 1.0,
@ -18,6 +21,8 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
final double extent; final double extent;
final double scale; final double scale;
final Object _cancellationKey = Uuid();
@override @override
Future<ThumbnailProviderKey> obtainKey(ImageConfiguration configuration) { Future<ThumbnailProviderKey> obtainKey(ImageConfiguration configuration) {
// configuration can be empty (e.g. when obtaining key for eviction) // configuration can be empty (e.g. when obtaining key for eviction)
@ -33,7 +38,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
@override @override
ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) { ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter( return CancellableMultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: key.scale, scale: key.scale,
informationCollector: () sync* { informationCollector: () sync* {
@ -44,12 +49,14 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async { Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
final dimPixels = (extent * key.devicePixelRatio).round(); final dimPixels = (extent * key.devicePixelRatio).round();
final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels); final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels, cancellationKey: _cancellationKey);
if (bytes.lengthInBytes == 0) { return await decode(bytes ?? Uint8List(0));
return null;
} }
return await decode(bytes); Future<void> cancel() async {
if (servicePolicy.cancel(_cancellationKey)) {
await evict();
}
} }
} }
@ -74,4 +81,30 @@ class ThumbnailProviderKey {
@override @override
int get hashCode => hashValues(entry.uri, extent, scale); int get hashCode => hashValues(entry.uri, extent, scale);
@override
String toString() {
return 'ThumbnailProviderKey{uri=${entry.uri}, extent=$extent, scale=$scale}';
}
}
class CancellableMultiFrameImageStreamCompleter extends MultiFrameImageStreamCompleter {
CancellableMultiFrameImageStreamCompleter({
@required Future<ui.Codec> codec,
@required double scale,
Stream<ImageChunkEvent> chunkEvents,
InformationCollector informationCollector,
}) : super(
codec: codec,
scale: scale,
chunkEvents: chunkEvents,
informationCollector: informationCollector,
);
@override
void reportError({DiagnosticsNode context, dynamic exception, StackTrace stack, informationCollector, bool silent = false}) {
// prevent default error reporting in case of planned cancellation
if (exception is CancelledException) return;
super.reportError(context: context, exception: exception, stack: stack, informationCollector: informationCollector, silent: silent);
}
} }

View file

@ -1,3 +1,4 @@
import 'dart:typed_data';
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui show Codec;
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/image_file_service.dart';
@ -36,11 +37,7 @@ class UriImage extends ImageProvider<UriImage> {
assert(key == this); assert(key == this);
final bytes = await ImageFileService.getImage(uri, mimeType); final bytes = await ImageFileService.getImage(uri, mimeType);
if (bytes.lengthInBytes == 0) { return await decode(bytes ?? Uint8List(0));
return null;
}
return await decode(bytes);
} }
@override @override

View file

@ -399,8 +399,8 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
@override @override
void dispose() { void dispose() {
super.dispose();
_unregisterWidget(widget); _unregisterWidget(widget);
super.dispose();
} }
void _registerWidget(FullscreenVerticalPageView widget) { void _registerWidget(FullscreenVerticalPageView widget) {

View file

@ -475,6 +475,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0+5" version: "0.9.0+5"
uuid:
dependency: "direct main"
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -20,12 +20,12 @@ dependencies:
charts_flutter: charts_flutter:
collection: collection:
draggable_scrollbar: draggable_scrollbar:
# path: ../flutter-draggable-scrollbar # path: ../flutter-draggable-scrollbar
git: git:
url: git://github.com/deckerst/flutter-draggable-scrollbar.git url: git://github.com/deckerst/flutter-draggable-scrollbar.git
event_bus: event_bus:
expansion_tile_card: expansion_tile_card:
# path: ../expansion_tile_card # path: ../expansion_tile_card
git: git:
url: git://github.com/deckerst/expansion_tile_card.git url: git://github.com/deckerst/expansion_tile_card.git
flushbar: flushbar:
@ -54,6 +54,7 @@ dependencies:
sqflite: sqflite:
transparent_image: transparent_image:
tuple: tuple:
uuid:
video_player: video_player:
path: ../plugins/packages/video_player/video_player path: ../plugins/packages/video_player/video_player