faster & smoother initial full screen load

This commit is contained in:
Thibault Deckers 2019-07-14 20:42:29 +09:00
parent c276c8b6b9
commit f43e861d05
6 changed files with 116 additions and 95 deletions

View file

@ -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
View 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}');
}
}
}

View file

@ -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,
),
],
), ),
); );
}); });

View file

@ -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),

View file

@ -1,7 +0,0 @@
final Settings settings = Settings._private();
class Settings {
double devicePixelRatio;
Settings._private();
}

View file

@ -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(