poc: load thumbnails with glide
This commit is contained in:
parent
d9695b0c68
commit
79e306a99c
9 changed files with 162 additions and 122 deletions
|
@ -54,6 +54,9 @@ flutter {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
implementation 'com.karumi:dexter:5.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
package deckers.thibault.aves;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.Key;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.karumi.dexter.Dexter;
|
||||
import com.karumi.dexter.PermissionToken;
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
||||
|
@ -27,47 +24,42 @@ import com.karumi.dexter.listener.PermissionRequest;
|
|||
import com.karumi.dexter.listener.single.PermissionListener;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.model.provider.MediaStoreImageProvider;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
class ThumbnailFetcher {
|
||||
private ContentResolver contentResolver;
|
||||
private SparseArray<AsyncTask> taskMap = new SparseArray<>();
|
||||
private Activity activity;
|
||||
private HashMap<String, AsyncTask> taskMap = new HashMap<>();
|
||||
|
||||
ThumbnailFetcher(ContentResolver contentResolver) {
|
||||
this.contentResolver = contentResolver;
|
||||
ThumbnailFetcher(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
void fetch (Integer id, Result result) {
|
||||
AsyncTask task = new BitmapWorkerTask(contentResolver).execute(new BitmapWorkerTask.MyTaskParams(id, result, this::complete));
|
||||
taskMap.append(id, task);
|
||||
void fetch(ImageEntry entry, Integer width, Integer height, Result result) {
|
||||
BitmapWorkerTask.MyTaskParams params = new BitmapWorkerTask.MyTaskParams(entry, width, height, result, this::complete);
|
||||
AsyncTask task = new BitmapWorkerTask(activity).execute(params);
|
||||
taskMap.put(entry.getUri().toString(), task);
|
||||
}
|
||||
|
||||
void cancel(Integer id) {
|
||||
AsyncTask task = taskMap.get(id, null);
|
||||
void cancel(String uri) {
|
||||
AsyncTask task = taskMap.get(uri);
|
||||
if (task != null) task.cancel(true);
|
||||
taskMap.remove(id);
|
||||
taskMap.remove(uri);
|
||||
}
|
||||
|
||||
void complete(Integer id) {
|
||||
taskMap.remove(id);
|
||||
private void complete(String uri) {
|
||||
taskMap.remove(uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +73,7 @@ public class MainActivity extends FlutterActivity {
|
|||
super.onCreate(savedInstanceState);
|
||||
GeneratedPluginRegistrant.registerWith(this);
|
||||
|
||||
thumbnailFetcher = new ThumbnailFetcher(getContentResolver());
|
||||
thumbnailFetcher = new ThumbnailFetcher(this);
|
||||
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
|
||||
(call, result) -> {
|
||||
switch (call.method) {
|
||||
|
@ -89,13 +81,16 @@ public class MainActivity extends FlutterActivity {
|
|||
getPermissionResult(result, this);
|
||||
break;
|
||||
case "getThumbnail": {
|
||||
Integer id = call.argument("id");
|
||||
thumbnailFetcher.fetch(id, result);
|
||||
Map map = call.argument("entry");
|
||||
Integer width = call.argument("width");
|
||||
Integer height = call.argument("height");
|
||||
ImageEntry entry = new ImageEntry(map);
|
||||
thumbnailFetcher.fetch(entry, width, height, result);
|
||||
break;
|
||||
}
|
||||
case "cancelGetThumbnail": {
|
||||
Integer id = call.argument("id");
|
||||
thumbnailFetcher.cancel(id);
|
||||
String uri = call.argument("uri");
|
||||
thumbnailFetcher.cancel(uri);
|
||||
result.success(null);
|
||||
break;
|
||||
}
|
||||
|
@ -106,21 +101,6 @@ public class MainActivity extends FlutterActivity {
|
|||
});
|
||||
}
|
||||
|
||||
// public void getImageThumbnail(final Result result, String uri, int width, int height) {
|
||||
// // https://developer.android.com/reference/android/content/ContentResolver.html#loadThumbnail(android.net.Uri,%20android.util.Size,%20android.os.CancellationSignal)
|
||||
// try {
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Bitmap bmp = getContentResolver().loadThumbnail(Uri.parse(uri), new Size(width, height), null);
|
||||
// result.success(bmp);
|
||||
// } else {
|
||||
// // TODO get by mediastore
|
||||
// getContentResolver().
|
||||
// }
|
||||
// } catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
|
||||
public void getPermissionResult(final Result result, final Activity activity) {
|
||||
Dexter.withActivity(activity)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
|
@ -137,47 +117,32 @@ public class MainActivity extends FlutterActivity {
|
|||
builder.setMessage("This permission is needed for use this features of the app so please, allow it!");
|
||||
builder.setTitle("We need this permission");
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
activity.startActivity(intent);
|
||||
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
builder.setPositiveButton("OK", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
activity.startActivity(intent);
|
||||
});
|
||||
builder.setNegativeButton("Cancel", (dialog, id) -> dialog.cancel());
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionRationaleShouldBeShown(PermissionRequest permission, final PermissionToken token) {
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage("This permission is needed for use this features of the app so please, allow it!");
|
||||
builder.setTitle("We need this permission");
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
token.continuePermissionRequest();
|
||||
|
||||
}
|
||||
builder.setPositiveButton("OK", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
token.continuePermissionRequest();
|
||||
});
|
||||
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
token.cancelPermissionRequest();
|
||||
}
|
||||
builder.setNegativeButton("Cancel", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
token.cancelPermissionRequest();
|
||||
});
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
|
@ -196,12 +161,15 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
|||
private static final String LOG_TAG = Utils.createLogTag(BitmapWorkerTask.class);
|
||||
|
||||
static class MyTaskParams {
|
||||
Integer id;
|
||||
ImageEntry entry;
|
||||
int width, height;
|
||||
Result result;
|
||||
Consumer<Integer> complete;
|
||||
Consumer<String> complete;
|
||||
|
||||
MyTaskParams(Integer id, Result result, Consumer<Integer> complete) {
|
||||
this.id = id;
|
||||
MyTaskParams(ImageEntry entry, int width, int height, Result result, Consumer<String> complete) {
|
||||
this.entry = entry;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.result = result;
|
||||
this.complete = complete;
|
||||
}
|
||||
|
@ -217,26 +185,40 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
|||
}
|
||||
}
|
||||
|
||||
private ContentResolver cr;
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private Activity activity;
|
||||
|
||||
BitmapWorkerTask(ContentResolver cr) {
|
||||
this.cr = cr;
|
||||
BitmapWorkerTask(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MyTaskResult doInBackground(MyTaskParams... params) {
|
||||
MyTaskParams p = params[0];
|
||||
ImageEntry entry = p.entry;
|
||||
byte[] data = null;
|
||||
if (!this.isCancelled()) {
|
||||
Log.d(LOG_TAG, "getThumbnail with id=" + p.id + "(called)");
|
||||
Bitmap bmp = MediaStore.Images.Thumbnails.getThumbnail(cr, p.id, MediaStore.Images.Thumbnails.MINI_KIND, null);
|
||||
if (bmp != null) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
data = stream.toByteArray();
|
||||
Log.d(LOG_TAG, "getThumbnail 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)
|
||||
.asBitmap()
|
||||
.load(entry.getUri())
|
||||
.signature(signature)
|
||||
.submit(p.width, p.height);
|
||||
try {
|
||||
Bitmap bmp = target.get();
|
||||
if (bmp != null) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
data = stream.toByteArray();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Glide.with(activity).clear(target);
|
||||
} else {
|
||||
Log.d(LOG_TAG, "getThumbnail with id=" + p.id + "(cancelled)");
|
||||
Log.d(LOG_TAG, "getThumbnail with uri=" + entry.getUri() + "(cancelled)");
|
||||
}
|
||||
return new MyTaskResult(p, data);
|
||||
}
|
||||
|
@ -244,11 +226,11 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
|||
@Override
|
||||
protected void onPostExecute(MyTaskResult result) {
|
||||
MethodChannel.Result r = result.params.result;
|
||||
result.params.complete.accept(result.params.id);
|
||||
result.params.complete.accept(result.params.entry.getUri().toString());
|
||||
if (result.data != null) {
|
||||
r.success(result.data);
|
||||
} else {
|
||||
r.error("getthumbnail-null", "failed to get thumbnail for id=" + result.params.id, null);
|
||||
r.error("getthumbnail-null", "failed to get thumbnail for uri=" + result.params.entry.getUri(), null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import android.content.ContentUris;
|
|||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
|
@ -58,11 +57,27 @@ public class ImageEntry {
|
|||
init();
|
||||
}
|
||||
|
||||
public ImageEntry(Map map) {
|
||||
this(
|
||||
(String) map.get("path"),
|
||||
toLong(map.get("contentId")),
|
||||
(String) map.get("mimeType"),
|
||||
(int) map.get("width"),
|
||||
(int) map.get("height"),
|
||||
(int) map.get("orientationDegrees"),
|
||||
toLong(map.get("sizeBytes")),
|
||||
(String) map.get("title"),
|
||||
toLong(map.get("dateModifiedSecs")),
|
||||
toLong(map.get("sourceDateTakenMillis")),
|
||||
(String) map.get("bucketDisplayName"),
|
||||
toLong(map.get("durationMillis"))
|
||||
);
|
||||
}
|
||||
|
||||
public static Map toMap(ImageEntry entry) {
|
||||
return new HashMap<String, Object>() {{
|
||||
put("path", entry.path);
|
||||
put("contentId", entry.contentId);
|
||||
put("uri", entry.uri);
|
||||
put("mimeType", entry.mimeType);
|
||||
put("width", entry.width);
|
||||
put("height", entry.height);
|
||||
|
@ -73,6 +88,8 @@ public class ImageEntry {
|
|||
put("sourceDateTakenMillis", entry.sourceDateTakenMillis);
|
||||
put("bucketDisplayName", entry.bucketDisplayName);
|
||||
put("durationMillis", entry.durationMillis);
|
||||
//
|
||||
put("uri", entry.getUri().toString());
|
||||
}};
|
||||
}
|
||||
|
||||
|
@ -174,4 +191,11 @@ public class ImageEntry {
|
|||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
// convenience method
|
||||
|
||||
private static long toLong(Object o) {
|
||||
if (o instanceof Integer) return Long.valueOf((Integer)o);
|
||||
return (long)o;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
classpath 'com.android.tools.build:gradle:3.4.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,15 +2,15 @@ import 'package:aves/thumbnail.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageFullscreenPage extends StatelessWidget {
|
||||
final int id;
|
||||
final Map entry;
|
||||
|
||||
ImageFullscreenPage({this.id});
|
||||
ImageFullscreenPage({this.entry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var width = MediaQuery.of(context).size.width;
|
||||
return Thumbnail(
|
||||
id: id,
|
||||
entry: entry,
|
||||
extent: width,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ class MyApp extends StatelessWidget {
|
|||
class ImageFetcher {
|
||||
static const platform = const MethodChannel('deckers.thibault.aves/mediastore');
|
||||
|
||||
static double devicePixelRatio;
|
||||
|
||||
static Future<List> getImages() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getImages');
|
||||
|
@ -35,10 +37,12 @@ class ImageFetcher {
|
|||
return [];
|
||||
}
|
||||
|
||||
static Future<Uint8List> getThumbnail(int id) async {
|
||||
static Future<Uint8List> getThumbnail(Map entry, double width, double height) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
||||
'id': id,
|
||||
'entry': entry,
|
||||
'width': (width * devicePixelRatio).round(),
|
||||
'height': (height * devicePixelRatio).round(),
|
||||
});
|
||||
return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
|
@ -47,10 +51,10 @@ class ImageFetcher {
|
|||
return Uint8List(0);
|
||||
}
|
||||
|
||||
static cancelGetThumbnail(int id) async {
|
||||
static cancelGetThumbnail(String uri) async {
|
||||
try {
|
||||
await platform.invokeMethod('cancelGetThumbnail', <String, dynamic>{
|
||||
'id': id,
|
||||
'uri': uri,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
|
@ -76,13 +80,17 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
super.initState();
|
||||
imageCache.maximumSizeBytes = 100 * 1024 * 1024;
|
||||
imageLoader = ImageFetcher.getImages();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
||||
ImageFetcher.devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
debugPrint('$runtimeType devicePixelRatio=${ImageFetcher.devicePixelRatio}');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var columnCount = 4;
|
||||
var spacing = 1.0;
|
||||
var extent = (MediaQuery.of(context).size.width - spacing * (columnCount - 1)) / columnCount;
|
||||
var extent = MediaQuery.of(context).size.width / columnCount;
|
||||
debugPrint('MediaQuery.of(context).size=${MediaQuery.of(context).size} extent=$extent');
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
@ -112,16 +120,13 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
controller: scrollController,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: extent,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
),
|
||||
itemBuilder: (gridContext, index) {
|
||||
var imageEntryMap = imageEntryList[index] as Map;
|
||||
var contentId = imageEntryMap['contentId'];
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ImageFullscreenPage(id: contentId)),
|
||||
MaterialPageRoute(builder: (context) => ImageFullscreenPage(entry: imageEntryMap)),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
|
@ -131,7 +136,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
),
|
||||
),
|
||||
child: Thumbnail(
|
||||
id: contentId,
|
||||
entry: imageEntryMap,
|
||||
extent: extent,
|
||||
),
|
||||
),
|
||||
|
|
6
lib/mime_types.dart
Normal file
6
lib/mime_types.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
class MimeTypes {
|
||||
static const String MIME_VIDEO = "video";
|
||||
static const String MIME_JPEG = "image/jpeg";
|
||||
static const String MIME_PNG = "image/png";
|
||||
static const String MIME_GIF = "image/gif";
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/main.dart';
|
||||
import 'package:aves/mime_types.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Thumbnail extends StatefulWidget {
|
||||
Thumbnail({Key key, @required this.id, @required this.extent}) : super(key: key);
|
||||
Thumbnail({Key key, @required this.entry, @required this.extent}) : super(key: key);
|
||||
|
||||
final int id;
|
||||
final Map entry;
|
||||
final double extent;
|
||||
|
||||
@override
|
||||
|
@ -20,29 +21,48 @@ class ThumbnailState extends State<Thumbnail> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loader = ImageFetcher.getThumbnail(widget.id);
|
||||
loader = ImageFetcher.getThumbnail(widget.entry, widget.extent, widget.extent);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ImageFetcher.cancelGetThumbnail(widget.id);
|
||||
ImageFetcher.cancelGetThumbnail(widget.entry['uri']);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String mimeType = widget.entry['mimeType'];
|
||||
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.id,
|
||||
child: Image.memory(
|
||||
bytes,
|
||||
width: widget.extent,
|
||||
height: widget.extent,
|
||||
fit: BoxFit.cover,
|
||||
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,
|
||||
),
|
||||
if (isGif)
|
||||
Icon(
|
||||
Icons.gif,
|
||||
size: iconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ description: A new Flutter application.
|
|||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.1.0 <3.0.0"
|
||||
sdk: ">=2.2.2 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
|
Loading…
Reference in a new issue