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.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 {
|
||||||
|
|
|
@ -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";
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>{
|
||||||
|
|
|
@ -50,8 +50,8 @@ class MetadataService {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
ServiceCallPriority.background,
|
priority: ServiceCallPriority.background,
|
||||||
'getCatalogMetadata-${entry.path}',
|
debugLabel: 'getCatalogMetadata-${entry.path}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue