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(
|
||||
(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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
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 '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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue