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 {
|
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 static final String CHANNEL = "deckers.thibault.aves/mediastore";
|
||||||
|
|
||||||
private ThumbnailFetcher thumbnailFetcher;
|
private ThumbnailFetcher thumbnailFetcher;
|
||||||
|
@ -85,6 +86,7 @@ public class MainActivity extends FlutterActivity {
|
||||||
Integer width = call.argument("width");
|
Integer width = call.argument("width");
|
||||||
Integer height = call.argument("height");
|
Integer height = call.argument("height");
|
||||||
ImageEntry entry = new ImageEntry(map);
|
ImageEntry entry = new ImageEntry(map);
|
||||||
|
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri());
|
||||||
thumbnailFetcher.fetch(entry, width, height, result);
|
thumbnailFetcher.fetch(entry, width, height, result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -198,7 +200,6 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
||||||
ImageEntry entry = p.entry;
|
ImageEntry entry = p.entry;
|
||||||
byte[] data = null;
|
byte[] data = null;
|
||||||
if (!this.isCancelled()) {
|
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
|
// 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());
|
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||||
|
@ -218,7 +219,7 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
||||||
}
|
}
|
||||||
Glide.with(activity).clear(target);
|
Glide.with(activity).clear(target);
|
||||||
} else {
|
} 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);
|
return new MyTaskResult(p, data);
|
||||||
}
|
}
|
||||||
|
@ -226,11 +227,13 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(MyTaskResult result) {
|
protected void onPostExecute(MyTaskResult result) {
|
||||||
MethodChannel.Result r = result.params.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) {
|
if (result.data != null) {
|
||||||
|
Log.d(LOG_TAG, "return bytes for uri=" + uri);
|
||||||
r.success(result.data);
|
r.success(result.data);
|
||||||
} else {
|
} 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 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/main.dart';
|
import 'package:aves/image_fetcher.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ImageFullscreenPage extends StatefulWidget {
|
class ImageFullscreenPage extends StatefulWidget {
|
||||||
|
@ -18,14 +19,13 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
||||||
|
|
||||||
int get imageWidth => widget.entry['width'];
|
int get imageWidth => widget.entry['width'];
|
||||||
|
|
||||||
int get imageHeight => widget.entry['width'];
|
int get imageHeight => widget.entry['height'];
|
||||||
|
|
||||||
String get uri => widget.entry['uri'];
|
String get uri => widget.entry['uri'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
loader = ImageFetcher.getImageBytes(widget.entry, imageWidth, imageHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -36,21 +36,41 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return FutureBuilder(
|
||||||
future: loader,
|
future: loader,
|
||||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||||
var ready = snapshot.connectionState == ConnectionState.done && !snapshot.hasError;
|
var ready = snapshot.connectionState == ConnectionState.done && !snapshot.hasError;
|
||||||
Uint8List bytes = ready ? snapshot.data : widget.thumbnail;
|
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: uri,
|
tag: uri,
|
||||||
child: Center(
|
child: Stack(
|
||||||
child: Image.memory(
|
children: [
|
||||||
bytes,
|
Image.memory(
|
||||||
|
widget.thumbnail,
|
||||||
width: imageWidth.toDouble(),
|
width: imageWidth.toDouble(),
|
||||||
height: imageHeight.toDouble(),
|
height: imageHeight.toDouble(),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
gaplessPlayback: true,
|
|
||||||
),
|
),
|
||||||
|
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/image_fetcher.dart';
|
||||||
|
|
||||||
import 'package:aves/settings.dart';
|
|
||||||
import 'package:aves/thumbnail.dart';
|
import 'package:aves/thumbnail.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/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
void main() => runApp(MyApp());
|
void main() {
|
||||||
|
runApp(MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
@override
|
@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 {
|
class MyHomePage extends StatefulWidget {
|
||||||
MyHomePage({Key key, this.title}) : super(key: key);
|
MyHomePage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
@ -77,19 +39,15 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
imageCache.maximumSizeBytes = 100 * 1024 * 1024;
|
imageCache.maximumSizeBytes = 100 * 1024 * 1024;
|
||||||
imageLoader = ImageFetcher.getImageEntries();
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
|
||||||
settings.devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
|
||||||
debugPrint('$runtimeType devicePixelRatio=${settings.devicePixelRatio}');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (imageLoader == null) {
|
||||||
|
imageLoader = ImageFetcher.getImageEntries();
|
||||||
|
}
|
||||||
var columnCount = 4;
|
var columnCount = 4;
|
||||||
var extent = MediaQuery.of(context).size.width / columnCount;
|
var extent = MediaQuery.of(context).size.width / columnCount;
|
||||||
debugPrint('MediaQuery.of(context).size=${MediaQuery.of(context).size} extent=$extent');
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.title),
|
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:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:aves/image_fetcher.dart';
|
||||||
import 'package:aves/image_fullscreen_page.dart';
|
import 'package:aves/image_fullscreen_page.dart';
|
||||||
import 'package:aves/main.dart';
|
|
||||||
import 'package:aves/mime_types.dart';
|
import 'package:aves/mime_types.dart';
|
||||||
import 'package:aves/settings.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
import 'package:transparent_image/transparent_image.dart';
|
||||||
|
|
||||||
|
@ -22,13 +21,17 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
Future<Uint8List> loader;
|
Future<Uint8List> loader;
|
||||||
Uint8List bytes;
|
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'];
|
String get uri => widget.entry['uri'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
var dim = (widget.extent * settings.devicePixelRatio).round();
|
|
||||||
loader = ImageFetcher.getImageBytes(widget.entry, dim, dim);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -39,7 +42,11 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
||||||
var isGif = mimeType == MimeTypes.MIME_GIF;
|
var isGif = mimeType == MimeTypes.MIME_GIF;
|
||||||
var iconSize = widget.extent / 4;
|
var iconSize = widget.extent / 4;
|
||||||
|
@ -68,8 +75,7 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
children: [
|
children: [
|
||||||
Hero(
|
Hero(
|
||||||
tag: uri,
|
tag: uri,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
builder: (context, constraints) {
|
|
||||||
// during hero animation back from a fullscreen image,
|
// 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)
|
// 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
|
// so we wrap the image to apply better constraints
|
||||||
|
@ -84,8 +90,7 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (isVideo)
|
if (isVideo)
|
||||||
Icon(
|
Icon(
|
||||||
|
|
Loading…
Reference in a new issue