poc: hero between thumbnail & full screen
This commit is contained in:
parent
79e306a99c
commit
c276c8b6b9
5 changed files with 148 additions and 81 deletions
|
@ -77,10 +77,10 @@ public class MainActivity extends FlutterActivity {
|
||||||
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
|
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
|
||||||
(call, result) -> {
|
(call, result) -> {
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case "getImages":
|
case "getImageEntries":
|
||||||
getPermissionResult(result, this);
|
getPermissionResult(result, this);
|
||||||
break;
|
break;
|
||||||
case "getThumbnail": {
|
case "getImageBytes": {
|
||||||
Map map = call.argument("entry");
|
Map map = call.argument("entry");
|
||||||
Integer width = call.argument("width");
|
Integer width = call.argument("width");
|
||||||
Integer height = call.argument("height");
|
Integer height = call.argument("height");
|
||||||
|
@ -88,7 +88,7 @@ public class MainActivity extends FlutterActivity {
|
||||||
thumbnailFetcher.fetch(entry, width, height, result);
|
thumbnailFetcher.fetch(entry, width, height, result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "cancelGetThumbnail": {
|
case "cancelGetImageBytes": {
|
||||||
String uri = call.argument("uri");
|
String uri = call.argument("uri");
|
||||||
thumbnailFetcher.cancel(uri);
|
thumbnailFetcher.cancel(uri);
|
||||||
result.success(null);
|
result.success(null);
|
||||||
|
@ -198,7 +198,7 @@ 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, "getThumbnail with uri=" + entry.getUri() + "(called)");
|
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 +218,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, "getThumbnail 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);
|
||||||
}
|
}
|
||||||
|
@ -230,7 +230,7 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
||||||
if (result.data != null) {
|
if (result.data != null) {
|
||||||
r.success(result.data);
|
r.success(result.data);
|
||||||
} else {
|
} else {
|
||||||
r.error("getthumbnail-null", "failed to get thumbnail for uri=" + result.params.entry.getUri(), null);
|
r.error("getImageBytes-null", "failed to get thumbnail for uri=" + result.params.entry.getUri(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,17 +1,58 @@
|
||||||
import 'package:aves/thumbnail.dart';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:aves/main.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ImageFullscreenPage extends StatelessWidget {
|
class ImageFullscreenPage extends StatefulWidget {
|
||||||
final Map entry;
|
final Map entry;
|
||||||
|
final Uint8List thumbnail;
|
||||||
|
|
||||||
ImageFullscreenPage({this.entry});
|
ImageFullscreenPage({this.entry, this.thumbnail});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageFullscreenPageState createState() => ImageFullscreenPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
||||||
|
Future<Uint8List> loader;
|
||||||
|
|
||||||
|
int get imageWidth => widget.entry['width'];
|
||||||
|
|
||||||
|
int get imageHeight => widget.entry['width'];
|
||||||
|
|
||||||
|
String get uri => widget.entry['uri'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
loader = ImageFetcher.getImageBytes(widget.entry, imageWidth, imageHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
ImageFetcher.cancelGetImageBytes(uri);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var width = MediaQuery.of(context).size.width;
|
return FutureBuilder(
|
||||||
return Thumbnail(
|
future: loader,
|
||||||
entry: entry,
|
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||||
extent: width,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:aves/image_fullscreen_page.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';
|
||||||
|
|
||||||
|
@ -25,11 +25,9 @@ class MyApp extends StatelessWidget {
|
||||||
class ImageFetcher {
|
class ImageFetcher {
|
||||||
static const platform = const MethodChannel('deckers.thibault.aves/mediastore');
|
static const platform = const MethodChannel('deckers.thibault.aves/mediastore');
|
||||||
|
|
||||||
static double devicePixelRatio;
|
static Future<List> getImageEntries() async {
|
||||||
|
|
||||||
static Future<List> getImages() async {
|
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getImages');
|
final result = await platform.invokeMethod('getImageEntries');
|
||||||
return result as List;
|
return result as List;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('failed with exception=${e.message}');
|
debugPrint('failed with exception=${e.message}');
|
||||||
|
@ -37,12 +35,12 @@ class ImageFetcher {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getThumbnail(Map entry, double width, double height) async {
|
static Future<Uint8List> getImageBytes(Map entry, int width, int height) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
|
||||||
'entry': entry,
|
'entry': entry,
|
||||||
'width': (width * devicePixelRatio).round(),
|
'width': width,
|
||||||
'height': (height * devicePixelRatio).round(),
|
'height': height,
|
||||||
});
|
});
|
||||||
return result as Uint8List;
|
return result as Uint8List;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -51,9 +49,9 @@ class ImageFetcher {
|
||||||
return Uint8List(0);
|
return Uint8List(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static cancelGetThumbnail(String uri) async {
|
static cancelGetImageBytes(String uri) async {
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('cancelGetThumbnail', <String, dynamic>{
|
await platform.invokeMethod('cancelGetImageBytes', <String, dynamic>{
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
});
|
});
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -79,11 +77,11 @@ 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.getImages();
|
imageLoader = ImageFetcher.getImageEntries();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
||||||
ImageFetcher.devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
settings.devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
debugPrint('$runtimeType devicePixelRatio=${ImageFetcher.devicePixelRatio}');
|
debugPrint('$runtimeType devicePixelRatio=${settings.devicePixelRatio}');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,24 +120,9 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
maxCrossAxisExtent: extent,
|
maxCrossAxisExtent: extent,
|
||||||
),
|
),
|
||||||
itemBuilder: (gridContext, index) {
|
itemBuilder: (gridContext, index) {
|
||||||
var imageEntryMap = imageEntryList[index] as Map;
|
return Thumbnail(
|
||||||
return GestureDetector(
|
entry: imageEntryList[index] as Map,
|
||||||
onTap: () => Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => ImageFullscreenPage(entry: imageEntryMap)),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Thumbnail(
|
|
||||||
entry: imageEntryMap,
|
|
||||||
extent: extent,
|
extent: extent,
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: imageEntryList.length,
|
itemCount: imageEntryList.length,
|
||||||
|
|
7
lib/settings.dart
Normal file
7
lib/settings.dart
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
final Settings settings = Settings._private();
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
double devicePixelRatio;
|
||||||
|
|
||||||
|
Settings._private();
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:aves/image_fullscreen_page.dart';
|
||||||
import 'package:aves/main.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';
|
||||||
|
|
||||||
|
@ -17,16 +20,20 @@ class Thumbnail extends StatefulWidget {
|
||||||
|
|
||||||
class ThumbnailState extends State<Thumbnail> {
|
class ThumbnailState extends State<Thumbnail> {
|
||||||
Future<Uint8List> loader;
|
Future<Uint8List> loader;
|
||||||
|
Uint8List bytes;
|
||||||
|
|
||||||
|
String get uri => widget.entry['uri'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
loader = ImageFetcher.getThumbnail(widget.entry, widget.extent, widget.extent);
|
var dim = (widget.extent * settings.devicePixelRatio).round();
|
||||||
|
loader = ImageFetcher.getImageBytes(widget.entry, dim, dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
ImageFetcher.cancelGetThumbnail(widget.entry['uri']);
|
ImageFetcher.cancelGetImageBytes(uri);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,22 +43,50 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
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;
|
||||||
return FutureBuilder(
|
return GestureDetector(
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ImageFullscreenPage(entry: widget.entry, thumbnail: bytes),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: FutureBuilder(
|
||||||
future: loader,
|
future: loader,
|
||||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||||
var ready = snapshot.connectionState == ConnectionState.done && !snapshot.hasError;
|
if (bytes == null && snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||||
Uint8List bytes = ready ? snapshot.data : kTransparentImage;
|
bytes = snapshot.data;
|
||||||
return Hero(
|
}
|
||||||
tag: widget.entry['uri'],
|
return Stack(
|
||||||
child: Stack(
|
|
||||||
alignment: AlignmentDirectional.bottomStart,
|
alignment: AlignmentDirectional.bottomStart,
|
||||||
children: [
|
children: [
|
||||||
Image.memory(
|
Hero(
|
||||||
bytes,
|
tag: uri,
|
||||||
width: widget.extent,
|
child: LayoutBuilder(
|
||||||
height: widget.extent,
|
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,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
if (isVideo)
|
if (isVideo)
|
||||||
Icon(
|
Icon(
|
||||||
Icons.play_circle_outline,
|
Icons.play_circle_outline,
|
||||||
|
@ -63,8 +98,9 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue