faster & smoother initial full screen load
This commit is contained in:
parent
c276c8b6b9
commit
f43e861d05
6 changed files with 116 additions and 95 deletions
|
@ -64,6 +64,7 @@ class ThumbnailFetcher {
|
|||
}
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
private static final String LOG_TAG = Utils.createLogTag(MainActivity.class);
|
||||
private static final String CHANNEL = "deckers.thibault.aves/mediastore";
|
||||
|
||||
private ThumbnailFetcher thumbnailFetcher;
|
||||
|
@ -85,6 +86,7 @@ public class MainActivity extends FlutterActivity {
|
|||
Integer width = call.argument("width");
|
||||
Integer height = call.argument("height");
|
||||
ImageEntry entry = new ImageEntry(map);
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri());
|
||||
thumbnailFetcher.fetch(entry, width, height, result);
|
||||
break;
|
||||
}
|
||||
|
@ -198,7 +200,6 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
|||
ImageEntry entry = p.entry;
|
||||
byte[] data = null;
|
||||
if (!this.isCancelled()) {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + "(called)");
|
||||
// add signature to ignore cache for images which got modified but kept the same URI
|
||||
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
|
||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||
|
@ -218,7 +219,7 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
|||
}
|
||||
Glide.with(activity).clear(target);
|
||||
} else {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + "(cancelled)");
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " (cancelled)");
|
||||
}
|
||||
return new MyTaskResult(p, data);
|
||||
}
|
||||
|
@ -226,11 +227,13 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
|||
@Override
|
||||
protected void onPostExecute(MyTaskResult result) {
|
||||
MethodChannel.Result r = result.params.result;
|
||||
result.params.complete.accept(result.params.entry.getUri().toString());
|
||||
String uri = result.params.entry.getUri().toString();
|
||||
result.params.complete.accept(uri);
|
||||
if (result.data != null) {
|
||||
Log.d(LOG_TAG, "return bytes for uri=" + uri);
|
||||
r.success(result.data);
|
||||
} else {
|
||||
r.error("getImageBytes-null", "failed to get thumbnail for uri=" + result.params.entry.getUri(), null);
|
||||
r.error("getImageBytes-null", "failed to get thumbnail for uri=" + uri, null);
|
||||
}
|
||||
}
|
||||
}
|
42
lib/image_fetcher.dart
Normal file
42
lib/image_fetcher.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ImageFetcher {
|
||||
static const platform = const MethodChannel('deckers.thibault.aves/mediastore');
|
||||
|
||||
static Future<List> getImageEntries() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getImageEntries');
|
||||
return result as List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<Uint8List> getImageBytes(Map entry, int width, int height) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
|
||||
'entry': entry,
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
}
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
||||
static cancelGetImageBytes(String uri) async {
|
||||
try {
|
||||
await platform.invokeMethod('cancelGetImageBytes', <String, dynamic>{
|
||||
'uri': uri,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/main.dart';
|
||||
import 'package:aves/image_fetcher.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageFullscreenPage extends StatefulWidget {
|
||||
|
@ -18,14 +19,13 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
|||
|
||||
int get imageWidth => widget.entry['width'];
|
||||
|
||||
int get imageHeight => widget.entry['width'];
|
||||
int get imageHeight => widget.entry['height'];
|
||||
|
||||
String get uri => widget.entry['uri'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loader = ImageFetcher.getImageBytes(widget.entry, imageWidth, imageHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -36,21 +36,41 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (loader == null) {
|
||||
var mediaQuery = MediaQuery.of(context);
|
||||
var screenSize = mediaQuery.size;
|
||||
var dpr = mediaQuery.devicePixelRatio;
|
||||
var requestWidth = imageWidth * dpr;
|
||||
var requestHeight = imageHeight * dpr;
|
||||
if (imageWidth > screenSize.width || imageHeight > screenSize.height) {
|
||||
var ratio = max(imageWidth / screenSize.width, imageHeight / screenSize.height);
|
||||
requestWidth /= ratio;
|
||||
requestHeight /= ratio;
|
||||
}
|
||||
loader = ImageFetcher.getImageBytes(widget.entry, requestWidth.round(), requestHeight.round());
|
||||
}
|
||||
return FutureBuilder(
|
||||
future: loader,
|
||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||
var ready = snapshot.connectionState == ConnectionState.done && !snapshot.hasError;
|
||||
Uint8List bytes = ready ? snapshot.data : widget.thumbnail;
|
||||
return Hero(
|
||||
tag: uri,
|
||||
child: Center(
|
||||
child: Image.memory(
|
||||
bytes,
|
||||
width: imageWidth.toDouble(),
|
||||
height: imageHeight.toDouble(),
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.memory(
|
||||
widget.thumbnail,
|
||||
width: imageWidth.toDouble(),
|
||||
height: imageHeight.toDouble(),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
if (ready)
|
||||
Image.memory(
|
||||
snapshot.data,
|
||||
width: imageWidth.toDouble(),
|
||||
height: imageHeight.toDouble(),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/settings.dart';
|
||||
import 'package:aves/image_fetcher.dart';
|
||||
import 'package:aves/thumbnail.dart';
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
|
@ -22,44 +22,6 @@ class MyApp extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class ImageFetcher {
|
||||
static const platform = const MethodChannel('deckers.thibault.aves/mediastore');
|
||||
|
||||
static Future<List> getImageEntries() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getImageEntries');
|
||||
return result as List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<Uint8List> getImageBytes(Map entry, int width, int height) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
|
||||
'entry': entry,
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
}
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
||||
static cancelGetImageBytes(String uri) async {
|
||||
try {
|
||||
await platform.invokeMethod('cancelGetImageBytes', <String, dynamic>{
|
||||
'uri': uri,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
MyHomePage({Key key, this.title}) : super(key: key);
|
||||
|
||||
|
@ -77,19 +39,15 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
imageCache.maximumSizeBytes = 100 * 1024 * 1024;
|
||||
imageLoader = ImageFetcher.getImageEntries();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
||||
settings.devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
debugPrint('$runtimeType devicePixelRatio=${settings.devicePixelRatio}');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imageLoader == null) {
|
||||
imageLoader = ImageFetcher.getImageEntries();
|
||||
}
|
||||
var columnCount = 4;
|
||||
var extent = MediaQuery.of(context).size.width / columnCount;
|
||||
debugPrint('MediaQuery.of(context).size=${MediaQuery.of(context).size} extent=$extent');
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.title),
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
final Settings settings = Settings._private();
|
||||
|
||||
class Settings {
|
||||
double devicePixelRatio;
|
||||
|
||||
Settings._private();
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/image_fetcher.dart';
|
||||
import 'package:aves/image_fullscreen_page.dart';
|
||||
import 'package:aves/main.dart';
|
||||
import 'package:aves/mime_types.dart';
|
||||
import 'package:aves/settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
|
@ -22,13 +21,17 @@ class ThumbnailState extends State<Thumbnail> {
|
|||
Future<Uint8List> loader;
|
||||
Uint8List bytes;
|
||||
|
||||
int get imageWidth => widget.entry['width'];
|
||||
|
||||
int get imageHeight => widget.entry['height'];
|
||||
|
||||
String get mimeType => widget.entry['mimeType'];
|
||||
|
||||
String get uri => widget.entry['uri'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var dim = (widget.extent * settings.devicePixelRatio).round();
|
||||
loader = ImageFetcher.getImageBytes(widget.entry, dim, dim);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -39,7 +42,11 @@ class ThumbnailState extends State<Thumbnail> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String mimeType = widget.entry['mimeType'];
|
||||
if (loader == null) {
|
||||
var dim = (widget.extent * MediaQuery.of(context).devicePixelRatio).round();
|
||||
loader = ImageFetcher.getImageBytes(widget.entry, dim, dim);
|
||||
}
|
||||
|
||||
var isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
||||
var isGif = mimeType == MimeTypes.MIME_GIF;
|
||||
var iconSize = widget.extent / 4;
|
||||
|
@ -68,24 +75,22 @@ class ThumbnailState extends State<Thumbnail> {
|
|||
children: [
|
||||
Hero(
|
||||
tag: uri,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// during hero animation back from a fullscreen image,
|
||||
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
||||
// so we wrap the image to apply better constraints
|
||||
var dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
constraints: BoxConstraints.tight(Size(dim, dim)),
|
||||
child: Image.memory(
|
||||
bytes ?? kTransparentImage,
|
||||
width: dim,
|
||||
height: dim,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
// during hero animation back from a fullscreen image,
|
||||
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
||||
// so we wrap the image to apply better constraints
|
||||
var dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
constraints: BoxConstraints.tight(Size(dim, dim)),
|
||||
child: Image.memory(
|
||||
bytes ?? kTransparentImage,
|
||||
width: dim,
|
||||
height: dim,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
if (isVideo)
|
||||
Icon(
|
||||
|
|
Loading…
Reference in a new issue