poc: hero between thumbnail & full screen

This commit is contained in:
Thibault Deckers 2019-07-14 13:34:44 +09:00
parent 79e306a99c
commit c276c8b6b9
5 changed files with 148 additions and 81 deletions

View file

@ -77,10 +77,10 @@ public class MainActivity extends FlutterActivity {
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
(call, result) -> {
switch (call.method) {
case "getImages":
case "getImageEntries":
getPermissionResult(result, this);
break;
case "getThumbnail": {
case "getImageBytes": {
Map map = call.argument("entry");
Integer width = call.argument("width");
Integer height = call.argument("height");
@ -88,7 +88,7 @@ public class MainActivity extends FlutterActivity {
thumbnailFetcher.fetch(entry, width, height, result);
break;
}
case "cancelGetThumbnail": {
case "cancelGetImageBytes": {
String uri = call.argument("uri");
thumbnailFetcher.cancel(uri);
result.success(null);
@ -198,7 +198,7 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
ImageEntry entry = p.entry;
byte[] data = null;
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
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
FutureTarget<Bitmap> target = Glide.with(activity)
@ -218,7 +218,7 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
}
Glide.with(activity).clear(target);
} 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);
}
@ -230,7 +230,7 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
if (result.data != null) {
r.success(result.data);
} 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);
}
}
}

View file

@ -1,17 +1,58 @@
import 'package:aves/thumbnail.dart';
import 'dart:typed_data';
import 'package:aves/main.dart';
import 'package:flutter/material.dart';
class ImageFullscreenPage extends StatelessWidget {
class ImageFullscreenPage extends StatefulWidget {
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
Widget build(BuildContext context) {
var width = MediaQuery.of(context).size.width;
return Thumbnail(
entry: entry,
extent: width,
);
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,
),
),
);
});
}
}

View file

@ -1,8 +1,8 @@
import 'dart:typed_data';
import 'package:aves/image_fullscreen_page.dart';
import 'package:aves/settings.dart';
import 'package:aves/thumbnail.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -25,11 +25,9 @@ class MyApp extends StatelessWidget {
class ImageFetcher {
static const platform = const MethodChannel('deckers.thibault.aves/mediastore');
static double devicePixelRatio;
static Future<List> getImages() async {
static Future<List> getImageEntries() async {
try {
final result = await platform.invokeMethod('getImages');
final result = await platform.invokeMethod('getImageEntries');
return result as List;
} on PlatformException catch (e) {
debugPrint('failed with exception=${e.message}');
@ -37,12 +35,12 @@ class ImageFetcher {
return [];
}
static Future<Uint8List> getThumbnail(Map entry, double width, double height) async {
static Future<Uint8List> getImageBytes(Map entry, int width, int height) async {
try {
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
'entry': entry,
'width': (width * devicePixelRatio).round(),
'height': (height * devicePixelRatio).round(),
'width': width,
'height': height,
});
return result as Uint8List;
} on PlatformException catch (e) {
@ -51,9 +49,9 @@ class ImageFetcher {
return Uint8List(0);
}
static cancelGetThumbnail(String uri) async {
static cancelGetImageBytes(String uri) async {
try {
await platform.invokeMethod('cancelGetThumbnail', <String, dynamic>{
await platform.invokeMethod('cancelGetImageBytes', <String, dynamic>{
'uri': uri,
});
} on PlatformException catch (e) {
@ -79,11 +77,11 @@ class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
imageCache.maximumSizeBytes = 100 * 1024 * 1024;
imageLoader = ImageFetcher.getImages();
imageLoader = ImageFetcher.getImageEntries();
WidgetsBinding.instance.addPostFrameCallback((duration) {
ImageFetcher.devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
debugPrint('$runtimeType devicePixelRatio=${ImageFetcher.devicePixelRatio}');
settings.devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
debugPrint('$runtimeType devicePixelRatio=${settings.devicePixelRatio}');
});
}
@ -112,9 +110,9 @@ class _MyHomePageState extends State<MyHomePage> {
padding: EdgeInsets.symmetric(vertical: 0.0),
child: DraggableScrollbar.arrows(
labelTextBuilder: (double offset) => Text(
"${offset ~/ 1}",
style: TextStyle(color: Colors.blueGrey),
),
"${offset ~/ 1}",
style: TextStyle(color: Colors.blueGrey),
),
controller: scrollController,
child: GridView.builder(
controller: scrollController,
@ -122,24 +120,9 @@ class _MyHomePageState extends State<MyHomePage> {
maxCrossAxisExtent: extent,
),
itemBuilder: (gridContext, index) {
var imageEntryMap = imageEntryList[index] as Map;
return GestureDetector(
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,
),
),
return Thumbnail(
entry: imageEntryList[index] as Map,
extent: extent,
);
},
itemCount: imageEntryList.length,

7
lib/settings.dart Normal file
View file

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

View file

@ -1,7 +1,10 @@
import 'dart:math';
import 'dart:typed_data';
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';
@ -17,16 +20,20 @@ class Thumbnail extends StatefulWidget {
class ThumbnailState extends State<Thumbnail> {
Future<Uint8List> loader;
Uint8List bytes;
String get uri => widget.entry['uri'];
@override
void 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
void dispose() {
ImageFetcher.cancelGetThumbnail(widget.entry['uri']);
ImageFetcher.cancelGetImageBytes(uri);
super.dispose();
}
@ -36,35 +43,64 @@ class ThumbnailState extends State<Thumbnail> {
var isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
var isGif = mimeType == MimeTypes.MIME_GIF;
var iconSize = widget.extent / 4;
return FutureBuilder(
future: loader,
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
var ready = snapshot.connectionState == ConnectionState.done && !snapshot.hasError;
Uint8List bytes = ready ? snapshot.data : kTransparentImage;
return Hero(
tag: widget.entry['uri'],
child: Stack(
alignment: AlignmentDirectional.bottomStart,
children: [
Image.memory(
bytes,
width: widget.extent,
height: widget.extent,
fit: BoxFit.cover,
),
if (isVideo)
Icon(
Icons.play_circle_outline,
size: iconSize,
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,
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
if (bytes == null && snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
bytes = snapshot.data;
}
return Stack(
alignment: AlignmentDirectional.bottomStart,
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,
),
);
}
),
),
if (isGif)
Icon(
Icons.gif,
size: iconSize,
),
],
),
);
});
if (isVideo)
Icon(
Icons.play_circle_outline,
size: iconSize,
),
if (isGif)
Icon(
Icons.gif,
size: iconSize,
),
],
);
}),
),
);
}
}