use Glide for a lower priority pass of higher quality thumbnails
This commit is contained in:
parent
f7ef4c0d01
commit
65fffdd21a
13 changed files with 189 additions and 122 deletions
|
@ -12,6 +12,7 @@ import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
@ -35,14 +36,15 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
|
|
||||||
static class Params {
|
static class Params {
|
||||||
ImageEntry entry;
|
ImageEntry entry;
|
||||||
int width, height;
|
Integer width, height, defaultSize;
|
||||||
MethodChannel.Result result;
|
MethodChannel.Result result;
|
||||||
|
|
||||||
Params(ImageEntry entry, int width, int height, MethodChannel.Result result) {
|
Params(ImageEntry entry, @Nullable Integer width, @Nullable Integer height, Integer defaultSize, 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.defaultSize = defaultSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +71,12 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
Bitmap bitmap = null;
|
Bitmap bitmap = null;
|
||||||
if (!this.isCancelled()) {
|
if (!this.isCancelled()) {
|
||||||
Exception exception = null;
|
Exception exception = null;
|
||||||
|
Integer w = p.width;
|
||||||
|
Integer h = p.height;
|
||||||
|
// fetch low quality thumbnails when size is not specified
|
||||||
|
if (w == null || h == null || w == 0 || h == 0) {
|
||||||
|
p.width = p.defaultSize;
|
||||||
|
p.height = p.defaultSize;
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
bitmap = getThumbnailBytesByResolver(p);
|
bitmap = getThumbnailBytesByResolver(p);
|
||||||
|
@ -78,8 +86,9 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
exception = e;
|
exception = e;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fallback if the native methods failed
|
// fallback if the native methods failed or for higher quality thumbnails
|
||||||
try {
|
try {
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
bitmap = getThumbnailByGlide(p);
|
bitmap = getThumbnailByGlide(p);
|
||||||
|
@ -108,8 +117,9 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||||
private Bitmap getThumbnailBytesByResolver(Params params) throws IOException {
|
private Bitmap getThumbnailBytesByResolver(Params params) throws IOException {
|
||||||
ImageEntry entry = params.entry;
|
ImageEntry entry = params.entry;
|
||||||
int width = params.width;
|
Integer width = params.width;
|
||||||
int height = params.height;
|
Integer height = params.height;
|
||||||
|
// Log.d(LOG_TAG, "getThumbnailBytesByResolver width=" + width + ", path=" + entry.path);
|
||||||
|
|
||||||
ContentResolver resolver = activity.getContentResolver();
|
ContentResolver resolver = activity.getContentResolver();
|
||||||
return resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
return resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
||||||
|
@ -141,6 +151,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
ImageEntry entry = params.entry;
|
ImageEntry entry = params.entry;
|
||||||
int width = params.width;
|
int width = params.width;
|
||||||
int height = params.height;
|
int height = params.height;
|
||||||
|
// Log.d(LOG_TAG, "getThumbnailByGlide width=" + width + ", path=" + entry.path);
|
||||||
|
|
||||||
// add signature to ignore cache for images which got modified but kept the same URI
|
// add signature to ignore cache for images which got modified but kept the same URI
|
||||||
Key signature = new ObjectKey("" + entry.dateModifiedSecs + entry.width + entry.orientationDegrees);
|
Key signature = new ObjectKey("" + entry.dateModifiedSecs + entry.width + entry.orientationDegrees);
|
||||||
|
|
|
@ -7,6 +7,8 @@ import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import deckers.thibault.aves.model.ImageEntry;
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
|
@ -39,6 +41,10 @@ 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 "clearSizedThumbnailDiskCache":
|
||||||
|
new Thread(() -> Glide.get(activity).clearDiskCache()).start();
|
||||||
|
result.success(null);
|
||||||
|
break;
|
||||||
case "rename":
|
case "rename":
|
||||||
new Thread(() -> rename(call, new MethodResultWrapper(result))).start();
|
new Thread(() -> rename(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
|
@ -55,12 +61,13 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
Map entryMap = call.argument("entry");
|
Map entryMap = call.argument("entry");
|
||||||
Integer width = call.argument("width");
|
Integer width = call.argument("width");
|
||||||
Integer height = call.argument("height");
|
Integer height = call.argument("height");
|
||||||
if (entryMap == null || width == null || height == null) {
|
Integer defaultSize = call.argument("defaultSize");
|
||||||
|
if (entryMap == null || defaultSize == null) {
|
||||||
result.error("getThumbnail-args", "failed because of missing arguments", null);
|
result.error("getThumbnail-args", "failed because of missing arguments", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ImageEntry entry = new ImageEntry(entryMap);
|
ImageEntry entry = new ImageEntry(entryMap);
|
||||||
new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(entry, width, height, result));
|
new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(entry, width, height, defaultSize, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
|
|
|
@ -266,7 +266,7 @@ class ImageEntry {
|
||||||
try {
|
try {
|
||||||
final addresses = await servicePolicy.call(
|
final addresses = await servicePolicy.call(
|
||||||
() => Geocoder.local.findAddressesFromCoordinates(coordinates),
|
() => Geocoder.local.findAddressesFromCoordinates(coordinates),
|
||||||
priority: ServiceCallPriority.background,
|
priority: ServiceCallPriority.getLocation,
|
||||||
);
|
);
|
||||||
if (addresses != null && addresses.isNotEmpty) {
|
if (addresses != null && addresses.isNotEmpty) {
|
||||||
final address = addresses.first;
|
final address = addresses.first;
|
||||||
|
|
|
@ -14,8 +14,6 @@ class ImageFileService {
|
||||||
static final StreamsChannel byteChannel = StreamsChannel('deckers.thibault/aves/imagebytestream');
|
static final StreamsChannel byteChannel = StreamsChannel('deckers.thibault/aves/imagebytestream');
|
||||||
static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream');
|
static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream');
|
||||||
|
|
||||||
static const thumbnailPriority = ServiceCallPriority.asap;
|
|
||||||
|
|
||||||
static Future<void> getImageEntries() async {
|
static Future<void> getImageEntries() async {
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('getImageEntries');
|
await platform.invokeMethod('getImageEntries');
|
||||||
|
@ -64,28 +62,36 @@ class ImageFileService {
|
||||||
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object taskKey}) {
|
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object taskKey}) {
|
||||||
return servicePolicy.call(
|
return servicePolicy.call(
|
||||||
() async {
|
() async {
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
||||||
'entry': entry.toMap(),
|
'entry': entry.toMap(),
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
|
'defaultSize': 256,
|
||||||
});
|
});
|
||||||
return result as Uint8List;
|
return result as Uint8List;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return Uint8List(0);
|
return Uint8List(0);
|
||||||
},
|
},
|
||||||
priority: thumbnailPriority,
|
// debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}',
|
||||||
|
priority: width == 0 || height == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail,
|
||||||
key: taskKey,
|
key: taskKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, thumbnailPriority);
|
static Future<void> clearSizedThumbnailDiskCache() async {
|
||||||
|
try {
|
||||||
|
return platform.invokeMethod('clearSizedThumbnailDiskCache');
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('clearSizedThumbnailDiskCache failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey, thumbnailPriority);
|
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]);
|
||||||
|
|
||||||
|
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
||||||
|
|
||||||
static Stream<ImageOpEvent> delete(Iterable<ImageEntry> entries) {
|
static Stream<ImageOpEvent> delete(Iterable<ImageEntry> entries) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -50,7 +50,7 @@ class MetadataService {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
priority: ServiceCallPriority.background,
|
priority: ServiceCallPriority.getMetadata,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,36 +2,37 @@ import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||||
|
|
||||||
class ServicePolicy {
|
class ServicePolicy {
|
||||||
final Map<Object, _Task> _paused = {};
|
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
||||||
final Queue<_Task> _asapQueue = Queue(), _normalQueue = Queue(), _backgroundQueue = Queue();
|
final SplayTreeMap<int, Queue<_Task>> _queues = SplayTreeMap();
|
||||||
List<Queue<_Task>> _queues;
|
|
||||||
_Task _running;
|
_Task _running;
|
||||||
|
|
||||||
ServicePolicy._private() {
|
ServicePolicy._private();
|
||||||
_queues = [_asapQueue, _normalQueue, _backgroundQueue];
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<T> call<T>(
|
Future<T> call<T>(
|
||||||
Future<T> Function() platformCall, {
|
Future<T> Function() platformCall, {
|
||||||
ServiceCallPriority priority = ServiceCallPriority.normal,
|
int priority = ServiceCallPriority.normal,
|
||||||
String debugLabel,
|
String debugLabel,
|
||||||
Object key,
|
Object key,
|
||||||
}) {
|
}) {
|
||||||
var task = _paused.remove(key);
|
_Task task;
|
||||||
if (task != null) {
|
final priorityTask = _paused.remove(key);
|
||||||
|
if (priorityTask != null) {
|
||||||
debugPrint('resume task with key=$key');
|
debugPrint('resume task with key=$key');
|
||||||
|
priority = priorityTask.item1;
|
||||||
|
task = priorityTask.item2;
|
||||||
}
|
}
|
||||||
var completer = task?.completer ?? Completer<T>();
|
var completer = task?.completer ?? Completer<T>();
|
||||||
task ??= _Task(
|
task ??= _Task(
|
||||||
() async {
|
() 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();
|
||||||
},
|
},
|
||||||
|
@ -43,62 +44,46 @@ class ServicePolicy {
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> resume<T>(Object key, ServiceCallPriority priority) {
|
Future<T> resume<T>(Object key) {
|
||||||
var task = _paused.remove(key);
|
final priorityTask = _paused.remove(key);
|
||||||
if (task == null) return null;
|
if (priorityTask == null) return null;
|
||||||
|
final priority = priorityTask.item1;
|
||||||
|
final task = priorityTask.item2;
|
||||||
_getQueue(priority).addLast(task);
|
_getQueue(priority).addLast(task);
|
||||||
_pickNext();
|
_pickNext();
|
||||||
return task.completer.future;
|
return task.completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Queue<_Task> _getQueue(ServiceCallPriority priority) {
|
Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>());
|
||||||
Queue<_Task> queue;
|
|
||||||
switch (priority) {
|
|
||||||
case ServiceCallPriority.asap:
|
|
||||||
queue = _asapQueue;
|
|
||||||
break;
|
|
||||||
case ServiceCallPriority.background:
|
|
||||||
queue = _backgroundQueue;
|
|
||||||
break;
|
|
||||||
case ServiceCallPriority.normal:
|
|
||||||
default:
|
|
||||||
queue = _normalQueue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return queue;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _pickNext() {
|
void _pickNext() {
|
||||||
if (_running != null) return;
|
if (_running != null) return;
|
||||||
final queue = _queues.firstWhere((q) => q.isNotEmpty, orElse: () => null);
|
final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value;
|
||||||
_running = queue?.removeFirst();
|
_running = queue?.removeFirst();
|
||||||
_running?.callback?.call();
|
_running?.callback?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool cancel(Object key, ServiceCallPriority priority) {
|
bool _takeOut(Object key, Iterable<int> priorities, void Function(int priority, _Task task) action) {
|
||||||
var cancelled = false;
|
var out = false;
|
||||||
|
priorities.forEach((priority) {
|
||||||
final queue = _getQueue(priority);
|
final queue = _getQueue(priority);
|
||||||
final tasks = queue.where((task) => task.key == key).toList();
|
final tasks = queue.where((task) => task.key == key).toList();
|
||||||
tasks.forEach((task) {
|
tasks.forEach((task) {
|
||||||
if (queue.remove(task)) {
|
if (queue.remove(task)) {
|
||||||
cancelled = true;
|
out = true;
|
||||||
task.completer.completeError(CancelledException());
|
action(priority, task);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return cancelled;
|
});
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool pause(Object key, ServiceCallPriority priority) {
|
bool cancel(Object key, Iterable<int> priorities) {
|
||||||
var paused = false;
|
return _takeOut(key, priorities, (priority, task) => task.completer.completeError(CancelledException()));
|
||||||
final queue = _getQueue(priority);
|
|
||||||
final tasks = queue.where((task) => task.key == key).toList();
|
|
||||||
tasks.forEach((task) {
|
|
||||||
if (queue.remove(task)) {
|
|
||||||
paused = true;
|
|
||||||
_paused.putIfAbsent(key, () => task);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return paused;
|
bool pause(Object key, Iterable<int> priorities) {
|
||||||
|
return _takeOut(key, priorities, (priority, task) => _paused.putIfAbsent(key, () => Tuple2(priority, task)));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPaused(Object key) => _paused.containsKey(key);
|
bool isPaused(Object key) => _paused.containsKey(key);
|
||||||
|
@ -114,4 +99,10 @@ class _Task {
|
||||||
|
|
||||||
class CancelledException {}
|
class CancelledException {}
|
||||||
|
|
||||||
enum ServiceCallPriority { asap, normal, background }
|
class ServiceCallPriority {
|
||||||
|
static const int getFastThumbnail = 100;
|
||||||
|
static const int getSizedThumbnail = 200;
|
||||||
|
static const int normal = 500;
|
||||||
|
static const int getMetadata = 1000;
|
||||||
|
static const int getLocation = 1000;
|
||||||
|
}
|
||||||
|
|
|
@ -22,9 +22,6 @@ class Constants {
|
||||||
// ref _PopupMenuRoute._kMenuDuration
|
// ref _PopupMenuRoute._kMenuDuration
|
||||||
static const popupMenuTransitionDuration = Duration(milliseconds: 300);
|
static const popupMenuTransitionDuration = Duration(milliseconds: 300);
|
||||||
|
|
||||||
// TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling
|
|
||||||
static const double thumbnailCacheExtent = 50;
|
|
||||||
|
|
||||||
static const svgBackground = Colors.white;
|
static const svgBackground = Colors.white;
|
||||||
static const svgColorFilter = ColorFilter.mode(svgBackground, BlendMode.dstOver);
|
static const svgColorFilter = ColorFilter.mode(svgBackground, BlendMode.dstOver);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
|
||||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/widgets/common/transition_image.dart';
|
import 'package:aves/widgets/common/transition_image.dart';
|
||||||
|
@ -24,7 +25,7 @@ class ThumbnailRasterImage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
ThumbnailProvider _imageProvider;
|
ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
@ -32,6 +33,11 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
|
|
||||||
Object get heroTag => widget.heroTag;
|
Object get heroTag => widget.heroTag;
|
||||||
|
|
||||||
|
// we standardize the thumbnail loading dimension by taking the nearest larger power of 2
|
||||||
|
// so that there are less variants of the thumbnails to load and cache
|
||||||
|
// it increases the chance of cache hit when loading similarly sized columns (e.g. on orientation change)
|
||||||
|
double get requestExtent => pow(2, (log(extent) / log(2)).ceil()).toDouble();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -53,7 +59,12 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initProvider() => _imageProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent);
|
void _initProvider() {
|
||||||
|
_fastThumbnailProvider = ThumbnailProvider(entry: entry);
|
||||||
|
if (!entry.isVideo) {
|
||||||
|
_sizedThumbnailProvider = ThumbnailProvider(entry: entry, extent: requestExtent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _pauseProvider() {
|
void _pauseProvider() {
|
||||||
final isScrolling = widget.isScrollingNotifier?.value ?? false;
|
final isScrolling = widget.isScrollingNotifier?.value ?? false;
|
||||||
|
@ -61,14 +72,26 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
// the retrieval task queue can pile up for thumbnails that got disposed
|
// the retrieval task queue can pile up for thumbnails that got disposed
|
||||||
// in this case we pause the image retrieval task to get it out of the queue
|
// in this case we pause the image retrieval task to get it out of the queue
|
||||||
if (isScrolling) {
|
if (isScrolling) {
|
||||||
_imageProvider?.pause();
|
_fastThumbnailProvider?.pause();
|
||||||
|
_sizedThumbnailProvider?.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final image = Image(
|
final fastImage = Image(
|
||||||
image: _imageProvider,
|
image: _fastThumbnailProvider,
|
||||||
|
width: extent,
|
||||||
|
height: extent,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
final image = _sizedThumbnailProvider == null
|
||||||
|
? fastImage
|
||||||
|
: Image(
|
||||||
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
|
return frame == null ? fastImage : child;
|
||||||
|
},
|
||||||
|
image: _sizedThumbnailProvider,
|
||||||
width: extent,
|
width: extent,
|
||||||
height: extent,
|
height: extent,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
@ -78,7 +101,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
: Hero(
|
: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
|
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
|
||||||
ImageProvider heroImageProvider = _imageProvider;
|
ImageProvider heroImageProvider = _fastThumbnailProvider;
|
||||||
if (!entry.isVideo && !entry.isSvg) {
|
if (!entry.isVideo && !entry.isSvg) {
|
||||||
final imageProvider = UriImage(
|
final imageProvider = UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:aves/widgets/common/scroll_thumb.dart';
|
import 'package:aves/widgets/common/scroll_thumb.dart';
|
||||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
final mqSize = mq.item1;
|
final mqSize = mq.item1;
|
||||||
final mqHorizontalPadding = mq.item2;
|
final mqHorizontalPadding = mq.item2;
|
||||||
TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier);
|
TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier);
|
||||||
|
final cacheExtent = TileExtentManager.extentMaxForSize(mqSize);
|
||||||
|
|
||||||
// do not replace by Provider.of<CollectionLens>
|
// do not replace by Provider.of<CollectionLens>
|
||||||
// so that view updates on collection filter changes
|
// so that view updates on collection filter changes
|
||||||
|
@ -47,6 +49,7 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
isScrollingNotifier: _isScrollingNotifier,
|
isScrollingNotifier: _isScrollingNotifier,
|
||||||
scrollController: PrimaryScrollController.of(context),
|
scrollController: PrimaryScrollController.of(context),
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
);
|
);
|
||||||
|
|
||||||
final scaler = GridScaleGestureDetector(
|
final scaler = GridScaleGestureDetector(
|
||||||
|
@ -91,6 +94,7 @@ class CollectionScrollView extends StatefulWidget {
|
||||||
final ValueNotifier<double> appBarHeightNotifier;
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
final ValueNotifier<bool> isScrollingNotifier;
|
final ValueNotifier<bool> isScrollingNotifier;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
|
final double cacheExtent;
|
||||||
|
|
||||||
const CollectionScrollView({
|
const CollectionScrollView({
|
||||||
@required this.scrollableKey,
|
@required this.scrollableKey,
|
||||||
|
@ -99,6 +103,7 @@ class CollectionScrollView extends StatefulWidget {
|
||||||
@required this.appBarHeightNotifier,
|
@required this.appBarHeightNotifier,
|
||||||
@required this.isScrollingNotifier,
|
@required this.isScrollingNotifier,
|
||||||
@required this.scrollController,
|
@required this.scrollController,
|
||||||
|
@required this.cacheExtent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -149,6 +154,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
||||||
// workaround to prevent scrolling the app bar away
|
// workaround to prevent scrolling the app bar away
|
||||||
// when there is no content and we use `SliverFillRemaining`
|
// when there is no content and we use `SliverFillRemaining`
|
||||||
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
|
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
|
||||||
|
cacheExtent: widget.cacheExtent,
|
||||||
slivers: [
|
slivers: [
|
||||||
appBar,
|
appBar,
|
||||||
collection.isEmpty
|
collection.isEmpty
|
||||||
|
|
|
@ -9,8 +9,8 @@ import 'package:flutter/material.dart';
|
||||||
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
ThumbnailProvider({
|
ThumbnailProvider({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.extent,
|
this.extent = 0,
|
||||||
this.scale = 1.0,
|
this.scale = 1,
|
||||||
}) : assert(entry != null),
|
}) : assert(entry != null),
|
||||||
assert(extent != null),
|
assert(extent != null),
|
||||||
assert(scale != null) {
|
assert(scale != null) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/android_file_service.dart';
|
||||||
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||||
|
@ -65,7 +66,7 @@ class DebugPageState extends State<DebugPage> {
|
||||||
Tab(icon: Icon(OMIcons.whatshot)),
|
Tab(icon: Icon(OMIcons.whatshot)),
|
||||||
Tab(icon: Icon(OMIcons.settings)),
|
Tab(icon: Icon(OMIcons.settings)),
|
||||||
Tab(icon: Icon(OMIcons.sdStorage)),
|
Tab(icon: Icon(OMIcons.sdStorage)),
|
||||||
Tab(text: 'Env'),
|
Tab(icon: Icon(OMIcons.android)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -108,8 +109,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('Image cache: ${imageCache.currentSize} items, ${formatFilesize(imageCache.currentSizeBytes)}'),
|
Expanded(
|
||||||
const Spacer(),
|
child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t${formatFilesize(imageCache.currentSizeBytes)}/${formatFilesize(imageCache.maximumSizeBytes)}'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
imageCache.clear();
|
imageCache.clear();
|
||||||
|
@ -121,8 +124,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('SVG cache: ${PictureProvider.cacheCount} items'),
|
Expanded(
|
||||||
const Spacer(),
|
child: Text('SVG cache: ${PictureProvider.cacheCount} items'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
PictureProvider.clearCache();
|
PictureProvider.clearCache();
|
||||||
|
@ -132,6 +137,18 @@ class DebugPageState extends State<DebugPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text('Glide disk cache: ?'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
RaisedButton(
|
||||||
|
onPressed: () => ImageFileService.clearSizedThumbnailDiskCache(),
|
||||||
|
child: const Text('Clear'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: _dbFileSizeLoader,
|
future: _dbFileSizeLoader,
|
||||||
|
@ -140,8 +157,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text('DB file size: ${formatFilesize(snapshot.data)}'),
|
Expanded(
|
||||||
const Spacer(),
|
child: Text('DB file size: ${formatFilesize(snapshot.data)}'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () => metadataDb.reset().then((_) => _startDbReport()),
|
onPressed: () => metadataDb.reset().then((_) => _startDbReport()),
|
||||||
child: const Text('Reset'),
|
child: const Text('Reset'),
|
||||||
|
@ -157,8 +176,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text('DB date rows: ${snapshot.data.length}'),
|
Expanded(
|
||||||
const Spacer(),
|
child: Text('DB date rows: ${snapshot.data.length}'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () => metadataDb.clearDates().then((_) => _startDbReport()),
|
onPressed: () => metadataDb.clearDates().then((_) => _startDbReport()),
|
||||||
child: const Text('Clear'),
|
child: const Text('Clear'),
|
||||||
|
@ -174,8 +195,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text('DB metadata rows: ${snapshot.data.length}'),
|
Expanded(
|
||||||
const Spacer(),
|
child: Text('DB metadata rows: ${snapshot.data.length}'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () => metadataDb.clearMetadataEntries().then((_) => _startDbReport()),
|
onPressed: () => metadataDb.clearMetadataEntries().then((_) => _startDbReport()),
|
||||||
child: const Text('Clear'),
|
child: const Text('Clear'),
|
||||||
|
@ -191,8 +214,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text('DB address rows: ${snapshot.data.length}'),
|
Expanded(
|
||||||
const Spacer(),
|
child: Text('DB address rows: ${snapshot.data.length}'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () => metadataDb.clearAddresses().then((_) => _startDbReport()),
|
onPressed: () => metadataDb.clearAddresses().then((_) => _startDbReport()),
|
||||||
child: const Text('Clear'),
|
child: const Text('Clear'),
|
||||||
|
@ -208,8 +233,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text('DB favourite rows: ${snapshot.data.length} (${favourites.count} in memory)'),
|
Expanded(
|
||||||
const Spacer(),
|
child: Text('DB favourite rows: ${snapshot.data.length} (${favourites.count} in memory)'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () => favourites.clear().then((_) => _startDbReport()),
|
onPressed: () => favourites.clear().then((_) => _startDbReport()),
|
||||||
child: const Text('Clear'),
|
child: const Text('Clear'),
|
||||||
|
@ -228,8 +255,10 @@ class DebugPageState extends State<DebugPage> {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Text('Settings'),
|
const Expanded(
|
||||||
const Spacer(),
|
child: Text('Settings'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () => settings.reset().then((_) => setState(() {})),
|
onPressed: () => settings.reset().then((_) => setState(() {})),
|
||||||
child: const Text('Reset'),
|
child: const Text('Reset'),
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart';
|
import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||||
|
@ -477,7 +476,8 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
||||||
|
|
||||||
void _onImageChanged() async {
|
void _onImageChanged() async {
|
||||||
await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict();
|
await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict();
|
||||||
await ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent).evict();
|
// TODO TLAD also evict `ThumbnailProvider` with specified extents
|
||||||
|
await ThumbnailProvider(entry: entry).evict();
|
||||||
if (entry.path != null) await FileImage(File(entry.path)).evict();
|
if (entry.path != null) await FileImage(File(entry.path)).evict();
|
||||||
// rebuild to refresh the Image inside ImagePage
|
// rebuild to refresh the Image inside ImagePage
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
|
@ -56,10 +56,7 @@ class ImageView extends StatelessWidget {
|
||||||
// if the hero tag wraps the whole `PhotoView` and the `loadingBuilder` is not provided,
|
// if the hero tag wraps the whole `PhotoView` and the `loadingBuilder` is not provided,
|
||||||
// there's a black frame between the hero animation and the final image, even when it's cached.
|
// there's a black frame between the hero animation and the final image, even when it's cached.
|
||||||
|
|
||||||
final thumbnailProvider = ThumbnailProvider(
|
final fastThumbnailProvider = ThumbnailProvider(entry: entry);
|
||||||
entry: entry,
|
|
||||||
extent: Constants.thumbnailCacheExtent,
|
|
||||||
);
|
|
||||||
// this loading builder shows a transition image until the final image is ready
|
// this loading builder shows a transition image until the final image is ready
|
||||||
// if the image is already in the cache it will show the final image, otherwise the thumbnail
|
// if the image is already in the cache it will show the final image, otherwise the thumbnail
|
||||||
// in any case, we should use `Center` + `AspectRatio` + `Fill` so that the transition image
|
// in any case, we should use `Center` + `AspectRatio` + `Fill` so that the transition image
|
||||||
|
@ -86,7 +83,7 @@ class ImageView extends StatelessWidget {
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
colorFilter: Constants.svgColorFilter,
|
colorFilter: Constants.svgColorFilter,
|
||||||
),
|
),
|
||||||
placeholderBuilder: (context) => loadingBuilder(context, thumbnailProvider),
|
placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider),
|
||||||
),
|
),
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
scaleStateChangedCallback: onScaleChanged,
|
scaleStateChangedCallback: onScaleChanged,
|
||||||
|
@ -107,7 +104,7 @@ class ImageView extends StatelessWidget {
|
||||||
// we still provide a `loadingBuilder` in that case to avoid a black frame after hero animation
|
// we still provide a `loadingBuilder` in that case to avoid a black frame after hero animation
|
||||||
loadingBuilder: (context, event) => loadingBuilder(
|
loadingBuilder: (context, event) => loadingBuilder(
|
||||||
context,
|
context,
|
||||||
imageCache.statusForKey(uriImage).keepAlive ? uriImage : thumbnailProvider,
|
imageCache.statusForKey(uriImage).keepAlive ? uriImage : fastThumbnailProvider,
|
||||||
),
|
),
|
||||||
loadFailedChild: const EmptyContent(
|
loadFailedChild: const EmptyContent(
|
||||||
icon: AIcons.error,
|
icon: AIcons.error,
|
||||||
|
|
Loading…
Reference in a new issue