thumbnail: cancel queued image loading on dispose
This commit is contained in:
parent
28e053cdd6
commit
19976940a0
18 changed files with 229 additions and 172 deletions
|
@ -23,7 +23,6 @@ import com.bumptech.glide.signature.ObjectKey;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import deckers.thibault.aves.decoder.VideoThumbnail;
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
|
@ -37,14 +36,12 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
ImageEntry entry;
|
||||
int width, height;
|
||||
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.width = width;
|
||||
this.height = height;
|
||||
this.result = result;
|
||||
this.complete = complete;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,11 +124,14 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
} else {
|
||||
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
|
||||
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.postRotate(entry.orientationDegrees);
|
||||
matrix.postRotate(orientationDegrees);
|
||||
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
||||
}
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
@ -176,7 +176,6 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
protected void onPostExecute(Result result) {
|
||||
MethodChannel.Result r = result.params.result;
|
||||
String uri = result.params.entry.uri.toString();
|
||||
result.params.complete.accept(uri);
|
||||
if (result.data != null) {
|
||||
r.success(result.data);
|
||||
} else {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -34,12 +34,10 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
public static final String CHANNEL = "deckers.thibault/aves/image";
|
||||
|
||||
private Activity activity;
|
||||
private ImageDecodeTaskManager imageDecodeTaskManager;
|
||||
private MediaStoreStreamHandler mediaStoreStreamHandler;
|
||||
|
||||
public ImageFileHandler(Activity activity, MediaStoreStreamHandler mediaStoreStreamHandler) {
|
||||
this.activity = activity;
|
||||
imageDecodeTaskManager = new ImageDecodeTaskManager(activity);
|
||||
this.mediaStoreStreamHandler = mediaStoreStreamHandler;
|
||||
}
|
||||
|
||||
|
@ -59,9 +57,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
case "getThumbnail":
|
||||
new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
case "cancelGetThumbnail":
|
||||
new Thread(() -> cancelGetThumbnail(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
case "delete":
|
||||
new Thread(() -> delete(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
|
@ -145,13 +140,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
return;
|
||||
}
|
||||
ImageEntry entry = new ImageEntry(entryMap);
|
||||
imageDecodeTaskManager.fetch(result, entry, width, height);
|
||||
}
|
||||
|
||||
private void cancelGetThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
String uri = call.argument("uri");
|
||||
imageDecodeTaskManager.cancel(uri);
|
||||
result.success(null);
|
||||
new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(entry, width, height, result));
|
||||
}
|
||||
|
||||
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
|
@ -275,7 +264,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
int bufferSize = 1024;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
|
||||
int len = 0;
|
||||
int len;
|
||||
while ((len = inputStream.read(buffer)) != -1) {
|
||||
byteBuffer.write(buffer, 0, len);
|
||||
}
|
||||
|
|
|
@ -223,8 +223,8 @@ class ImageEntry {
|
|||
try {
|
||||
final addresses = await servicePolicy.call(
|
||||
() => Geocoder.local.findAddressesFromCoordinates(coordinates),
|
||||
ServiceCallPriority.background,
|
||||
'findAddressesFromCoordinates-$path',
|
||||
priority: ServiceCallPriority.background,
|
||||
debugLabel: 'findAddressesFromCoordinates-$path',
|
||||
);
|
||||
if (addresses != null && addresses.isNotEmpty) {
|
||||
final address = addresses.first;
|
||||
|
|
|
@ -43,7 +43,7 @@ class ImageFileService {
|
|||
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(
|
||||
() async {
|
||||
if (width > 0 && height > 0) {
|
||||
|
@ -61,21 +61,12 @@ class ImageFileService {
|
|||
}
|
||||
return Uint8List(0);
|
||||
},
|
||||
ServiceCallPriority.asapLifo,
|
||||
'getThumbnail-${entry.path}',
|
||||
priority: ServiceCallPriority.asap,
|
||||
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 {
|
||||
try {
|
||||
await platform.invokeMethod('delete', <String, dynamic>{
|
||||
|
|
|
@ -50,8 +50,8 @@ class MetadataService {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
ServiceCallPriority.background,
|
||||
'getCatalogMetadata-${entry.path}',
|
||||
priority: ServiceCallPriority.background,
|
||||
debugLabel: 'getCatalogMetadata-${entry.path}',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,47 +1,55 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||
|
||||
class ServicePolicy {
|
||||
final Queue<VoidCallback> _asapQueue = Queue(), _normalQueue = Queue(), _backgroundQueue = Queue();
|
||||
VoidCallback _running;
|
||||
final Queue<_Task> _asapQueue, _normalQueue, _backgroundQueue;
|
||||
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]) {
|
||||
Queue<VoidCallback> q;
|
||||
Future<T> call<T>(
|
||||
Future<T> Function() platformCall, {
|
||||
ServiceCallPriority priority = ServiceCallPriority.normal,
|
||||
String debugLabel,
|
||||
Object cancellationKey,
|
||||
}) {
|
||||
Queue<_Task> queue;
|
||||
switch (priority) {
|
||||
case ServiceCallPriority.asapFifo:
|
||||
q = _asapQueue;
|
||||
break;
|
||||
case ServiceCallPriority.asapLifo:
|
||||
q = _asapQueue;
|
||||
case ServiceCallPriority.asap:
|
||||
queue = _asapQueue;
|
||||
break;
|
||||
case ServiceCallPriority.background:
|
||||
q = _backgroundQueue;
|
||||
queue = _backgroundQueue;
|
||||
break;
|
||||
case ServiceCallPriority.normal:
|
||||
default:
|
||||
q = _normalQueue;
|
||||
queue = _normalQueue;
|
||||
break;
|
||||
}
|
||||
final completer = Completer<T>();
|
||||
final wrapped = () async {
|
||||
final wrapped = _Task(
|
||||
() async {
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
|
||||
final result = await platformCall();
|
||||
completer.complete(result);
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
|
||||
_running = null;
|
||||
_pickNext();
|
||||
};
|
||||
if (priority == ServiceCallPriority.asapLifo) {
|
||||
q.addFirst(wrapped);
|
||||
} else {
|
||||
q.addLast(wrapped);
|
||||
}
|
||||
},
|
||||
completer,
|
||||
cancellationKey,
|
||||
);
|
||||
queue.addLast(wrapped);
|
||||
|
||||
_pickNext();
|
||||
return completer.future;
|
||||
|
@ -49,10 +57,32 @@ class ServicePolicy {
|
|||
|
||||
void _pickNext() {
|
||||
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?.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 }
|
||||
|
|
|
@ -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_date.dart';
|
||||
import 'package:aves/widgets/common/fx/outlined_text.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
|
|
|
@ -91,10 +91,12 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
|||
final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount);
|
||||
final children = <Widget>[];
|
||||
for (var i = minEntryIndex; i < maxEntryIndex; i++) {
|
||||
final entry = section[i];
|
||||
children.add(GridThumbnail(
|
||||
key: ValueKey(entry.contentId),
|
||||
collection: collection,
|
||||
index: i,
|
||||
entry: section[i],
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
));
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class GridThumbnail extends StatelessWidget {
|
|||
onTap: () => _goToFullscreen(context),
|
||||
child: MetaData(
|
||||
metaData: ThumbnailMetadata(index, entry),
|
||||
child: Thumbnail(
|
||||
child: DecoratedThumbnail(
|
||||
entry: entry,
|
||||
extent: tileExtent,
|
||||
heroTag: collection.heroTag(entry),
|
||||
|
|
|
@ -204,7 +204,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
|
|||
Positioned(
|
||||
left: clampedCenter.dx - extent / 2,
|
||||
top: clampedCenter.dy - extent / 2,
|
||||
child: Thumbnail(
|
||||
child: DecoratedThumbnail(
|
||||
entry: widget.imageEntry,
|
||||
extent: extent,
|
||||
),
|
||||
|
@ -232,12 +232,12 @@ class GridPainter extends CustomPainter {
|
|||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..strokeWidth = Thumbnail.borderWidth
|
||||
..strokeWidth = DecoratedThumbnail.borderWidth
|
||||
..shader = ui.Gradient.radial(
|
||||
center,
|
||||
size.width / 2,
|
||||
[
|
||||
Thumbnail.borderColor,
|
||||
DecoratedThumbnail.borderColor,
|
||||
Colors.transparent,
|
||||
],
|
||||
[
|
||||
|
|
|
@ -10,7 +10,7 @@ import 'package:aves/widgets/common/transition_image.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class Thumbnail extends StatelessWidget {
|
||||
class DecoratedThumbnail extends StatelessWidget {
|
||||
final ImageEntry entry;
|
||||
final double extent;
|
||||
final Object heroTag;
|
||||
|
@ -18,7 +18,7 @@ class Thumbnail extends StatelessWidget {
|
|||
static final Color borderColor = Colors.grey.shade700;
|
||||
static const double borderWidth = .5;
|
||||
|
||||
const Thumbnail({
|
||||
const DecoratedThumbnail({
|
||||
Key key,
|
||||
@required this.entry,
|
||||
@required this.extent,
|
||||
|
@ -39,7 +39,13 @@ class Thumbnail extends StatelessWidget {
|
|||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
entry.isSvg ? _buildVectorImage() : _buildRasterImage(),
|
||||
entry.isSvg
|
||||
? _buildVectorImage()
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
heroTag: heroTag,
|
||||
),
|
||||
_ThumbnailOverlay(
|
||||
entry: entry,
|
||||
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() {
|
||||
final child = Container(
|
||||
// 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 {
|
||||
final ImageEntry entry;
|
||||
final double extent;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
|
||||
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 {
|
||||
final bytes = await AndroidAppService.getAppIcon(key.packageName, key.sizePixels);
|
||||
if (bytes.lengthInBytes == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await decode(bytes);
|
||||
return await decode(bytes ?? Uint8List(0));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/services/service_policy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||
const ThumbnailProvider({
|
||||
ThumbnailProvider({
|
||||
@required this.entry,
|
||||
@required this.extent,
|
||||
this.scale = 1.0,
|
||||
|
@ -18,6 +21,8 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
final double extent;
|
||||
final double scale;
|
||||
|
||||
final Object _cancellationKey = Uuid();
|
||||
|
||||
@override
|
||||
Future<ThumbnailProviderKey> obtainKey(ImageConfiguration configuration) {
|
||||
// configuration can be empty (e.g. when obtaining key for eviction)
|
||||
|
@ -33,7 +38,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
|
||||
@override
|
||||
ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
return CancellableMultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
informationCollector: () sync* {
|
||||
|
@ -44,12 +49,14 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
|
||||
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
|
||||
final dimPixels = (extent * key.devicePixelRatio).round();
|
||||
final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels);
|
||||
if (bytes.lengthInBytes == 0) {
|
||||
return null;
|
||||
final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels, cancellationKey: _cancellationKey);
|
||||
return await decode(bytes ?? Uint8List(0));
|
||||
}
|
||||
|
||||
return await decode(bytes);
|
||||
Future<void> cancel() async {
|
||||
if (servicePolicy.cancel(_cancellationKey)) {
|
||||
await evict();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,4 +81,30 @@ class ThumbnailProviderKey {
|
|||
|
||||
@override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
|
@ -36,11 +37,7 @@ class UriImage extends ImageProvider<UriImage> {
|
|||
assert(key == this);
|
||||
|
||||
final bytes = await ImageFileService.getImage(uri, mimeType);
|
||||
if (bytes.lengthInBytes == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await decode(bytes);
|
||||
return await decode(bytes ?? Uint8List(0));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -399,8 +399,8 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(FullscreenVerticalPageView widget) {
|
||||
|
|
|
@ -475,6 +475,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -20,12 +20,12 @@ dependencies:
|
|||
charts_flutter:
|
||||
collection:
|
||||
draggable_scrollbar:
|
||||
# path: ../flutter-draggable-scrollbar
|
||||
# path: ../flutter-draggable-scrollbar
|
||||
git:
|
||||
url: git://github.com/deckerst/flutter-draggable-scrollbar.git
|
||||
event_bus:
|
||||
expansion_tile_card:
|
||||
# path: ../expansion_tile_card
|
||||
# path: ../expansion_tile_card
|
||||
git:
|
||||
url: git://github.com/deckerst/expansion_tile_card.git
|
||||
flushbar:
|
||||
|
@ -54,6 +54,7 @@ dependencies:
|
|||
sqflite:
|
||||
transparent_image:
|
||||
tuple:
|
||||
uuid:
|
||||
video_player:
|
||||
path: ../plugins/packages/video_player/video_player
|
||||
|
||||
|
|
Loading…
Reference in a new issue