fullscreen: work with uri/path & flutter image widget
This commit is contained in:
parent
1313d0c845
commit
10be8b1f2e
5 changed files with 282 additions and 69 deletions
|
@ -93,7 +93,8 @@ public class MainActivity extends FlutterActivity {
|
|||
case "cancelGetImageBytes": {
|
||||
String uri = call.argument("uri");
|
||||
thumbnailFetcher.cancel(uri);
|
||||
result.success(null);
|
||||
// do not send `null`, as it closes the channel
|
||||
result.success("");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -214,12 +215,14 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
|
|||
bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
data = stream.toByteArray();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " interrupted");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Glide.with(activity).clear(target);
|
||||
} 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);
|
||||
}
|
||||
|
|
|
@ -1,24 +1,16 @@
|
|||
package deckers.thibault.aves.model;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import deckers.thibault.aves.utils.Constants;
|
||||
|
||||
public class ImageEntry {
|
||||
private static final Uri mediaStoreContentUri = MediaStore.Files.getContentUri("external");
|
||||
|
||||
// from source
|
||||
private String path; // best effort to get local path from content providers
|
||||
private long contentId; // should be defined for mediastore, use full URI otherwise
|
||||
|
@ -39,11 +31,13 @@ public class ImageEntry {
|
|||
init();
|
||||
}
|
||||
|
||||
public ImageEntry(String path, long id, String mimeType, int width, int height, int orientationDegrees, long sizeBytes,
|
||||
// uri: content provider uri
|
||||
// path: FileUtils.getPathFromUri(activity, itemUri) is useful (for Download, File, etc.) but is slower than directly using `MediaStore.MediaColumns.DATA` from the MediaStore query
|
||||
public ImageEntry(Uri uri, String path, long id, String mimeType, int width, int height, int orientationDegrees, long sizeBytes,
|
||||
String title, long dateModifiedSecs, long dateTakenMillis, String bucketDisplayName, long durationMillis) {
|
||||
this.uri = uri;
|
||||
this.path = path;
|
||||
this.contentId = id;
|
||||
this.uri = null;
|
||||
this.mimeType = mimeType;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
@ -59,6 +53,7 @@ public class ImageEntry {
|
|||
|
||||
public ImageEntry(Map map) {
|
||||
this(
|
||||
Uri.parse((String) map.get("uri")),
|
||||
(String) map.get("path"),
|
||||
toLong(map.get("contentId")),
|
||||
(String) map.get("mimeType"),
|
||||
|
@ -76,6 +71,7 @@ public class ImageEntry {
|
|||
|
||||
public static Map toMap(ImageEntry entry) {
|
||||
return new HashMap<String, Object>() {{
|
||||
put("uri", entry.uri.toString());
|
||||
put("path", entry.path);
|
||||
put("contentId", entry.contentId);
|
||||
put("mimeType", entry.mimeType);
|
||||
|
@ -88,8 +84,6 @@ public class ImageEntry {
|
|||
put("sourceDateTakenMillis", entry.sourceDateTakenMillis);
|
||||
put("bucketDisplayName", entry.bucketDisplayName);
|
||||
put("durationMillis", entry.durationMillis);
|
||||
//
|
||||
put("uri", entry.getUri().toString());
|
||||
}};
|
||||
}
|
||||
|
||||
|
@ -97,6 +91,10 @@ public class ImageEntry {
|
|||
isVideo = mimeType.startsWith(Constants.MIME_VIDEO);
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPath() {
|
||||
return path;
|
||||
|
@ -106,18 +104,6 @@ public class ImageEntry {
|
|||
return path == null ? null : new File(path).getName();
|
||||
}
|
||||
|
||||
public InputStream getInputStream(Context context) throws FileNotFoundException {
|
||||
// FileInputStream is faster than input stream from ContentResolver
|
||||
return path != null ? new FileInputStream(path) : context.getContentResolver().openInputStream(getUri());
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
if (uri != null) {
|
||||
return uri;
|
||||
}
|
||||
return ContentUris.withAppendedId(mediaStoreContentUri, contentId);
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return isVideo;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package deckers.thibault.aves.model.provider;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentUris;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
|
@ -39,22 +40,24 @@ public class MediaStoreImageProvider {
|
|||
|
||||
|
||||
public List<ImageEntry> fetchAll(Activity activity) {
|
||||
return fetch(activity, FILES_URI, null);
|
||||
return fetch(activity, FILES_URI);
|
||||
}
|
||||
|
||||
public List<ImageEntry> fetch(final Activity activity, final Uri uri, String mimeType) {
|
||||
private List<ImageEntry> fetch(final Activity activity, final Uri queryUri) {
|
||||
ArrayList<ImageEntry> entries = new ArrayList<>();
|
||||
|
||||
// URI should refer to the "files" table, not to the "images" or "videos" one,
|
||||
// as our projection includes a mix of columns from both
|
||||
Uri filesUri = uri;
|
||||
if (!FILES_URI.equals(uri)) {
|
||||
String id = uri.getLastPathSegment();
|
||||
Uri filesUri = queryUri;
|
||||
if (!FILES_URI.equals(queryUri)) {
|
||||
String id = queryUri.getLastPathSegment();
|
||||
filesUri = Uri.withAppendedPath(FILES_URI, id);
|
||||
}
|
||||
|
||||
String orderBy = MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC";
|
||||
|
||||
try {
|
||||
Cursor cursor = activity.getContentResolver().query(filesUri, PROJECTION, SELECTION, null, null);
|
||||
Cursor cursor = activity.getContentResolver().query(filesUri, PROJECTION, SELECTION, null, orderBy);
|
||||
if (cursor != null) {
|
||||
Log.d(LOG_TAG, "fetch query returned " + cursor.getCount() + " entries");
|
||||
|
||||
|
@ -72,9 +75,12 @@ public class MediaStoreImageProvider {
|
|||
int durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
long contentId = cursor.getLong(idColumn);
|
||||
Uri itemUri = ContentUris.withAppendedId(FILES_URI, contentId);
|
||||
ImageEntry imageEntry = new ImageEntry(
|
||||
itemUri,
|
||||
cursor.getString(pathColumn),
|
||||
cursor.getLong(idColumn),
|
||||
contentId,
|
||||
cursor.getString(mimeTypeColumn),
|
||||
cursor.getInt(widthColumn),
|
||||
cursor.getInt(heightColumn),
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Copyright (C) 2007-2008 OpenIntents.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file was modified by the Flutter authors from the following original file:
|
||||
* https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
|
||||
*/
|
||||
|
||||
// TLAD: copied from https://raw.githubusercontent.com/flutter/plugins/master/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java
|
||||
|
||||
package deckers.thibault.aves.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
public String getPathFromUri(final Context context, final Uri uri) {
|
||||
String path = getPathFromLocalUri(context, uri);
|
||||
if (path == null) {
|
||||
path = getPathFromRemoteUri(context, uri);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private String getPathFromLocalUri(final Context context, final Uri uri) {
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
} else if (isDownloadsDocument(uri)) {
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
|
||||
if (!TextUtils.isEmpty(id)) {
|
||||
try {
|
||||
final Uri contentUri =
|
||||
ContentUris.withAppendedId(
|
||||
Uri.parse(Environment.DIRECTORY_DOWNLOADS), Long.valueOf(id));
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (isMediaDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[] {split[1]};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
|
||||
// Return the remote address
|
||||
if (isGooglePhotosUri(uri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getDataColumn(context, uri, null, null);
|
||||
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getDataColumn(
|
||||
Context context, Uri uri, String selection, String[] selectionArgs) {
|
||||
|
||||
final String column = "_data";
|
||||
final String[] projection = {column};
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int column_index = cursor.getColumnIndex(column);
|
||||
|
||||
//yandex.disk and dropbox do not have _data column
|
||||
if (column_index == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cursor.getString(column_index);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getPathFromRemoteUri(final Context context, final Uri uri) {
|
||||
// The code below is why Java now has try-with-resources and the Files utility.
|
||||
File file = null;
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
String extension = getImageExtension(context, uri);
|
||||
inputStream = context.getContentResolver().openInputStream(uri);
|
||||
file = File.createTempFile("image_picker", extension, context.getCacheDir());
|
||||
outputStream = new FileOutputStream(file);
|
||||
if (inputStream != null) {
|
||||
copy(inputStream, outputStream);
|
||||
success = true;
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
} finally {
|
||||
try {
|
||||
if (inputStream != null) inputStream.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
try {
|
||||
if (outputStream != null) outputStream.close();
|
||||
} catch (IOException ignored) {
|
||||
// If closing the output stream fails, we cannot be sure that the
|
||||
// target file was written in full. Flushing the stream merely moves
|
||||
// the bytes into the OS, not necessarily to the file.
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success ? file.getPath() : null;
|
||||
}
|
||||
|
||||
/** @return extension of image with dot, or default .jpg if it none. */
|
||||
private static String getImageExtension(Context context, Uri uriImage) {
|
||||
String extension = null;
|
||||
|
||||
try (Cursor cursor = context
|
||||
.getContentResolver()
|
||||
.query(uriImage, new String[]{MediaStore.MediaColumns.MIME_TYPE}, null, null, null)) {
|
||||
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
String mimeType = cursor.getString(0);
|
||||
|
||||
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
if (extension == null) {
|
||||
//default extension for matches the previous behavior of the plugin
|
||||
extension = "jpg";
|
||||
}
|
||||
return "." + extension;
|
||||
}
|
||||
|
||||
private static void copy(InputStream in, OutputStream out) throws IOException {
|
||||
final byte[] buffer = new byte[4 * 1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
private static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
private static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
private static boolean isGooglePhotosUri(Uri uri) {
|
||||
return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority());
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/image_fetcher.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
class ImageFullscreenPage extends StatefulWidget {
|
||||
final Map entry;
|
||||
|
@ -15,14 +16,16 @@ class ImageFullscreenPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
||||
Future<Uint8List> loader;
|
||||
|
||||
int get imageWidth => widget.entry['width'];
|
||||
|
||||
int get imageHeight => widget.entry['height'];
|
||||
|
||||
String get uri => widget.entry['uri'];
|
||||
|
||||
String get path => widget.entry['path'];
|
||||
|
||||
double requestWidth, requestHeight;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -30,30 +33,29 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
ImageFetcher.cancelGetImageBytes(uri);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (loader == null) {
|
||||
if (requestWidth == null || requestHeight == null) {
|
||||
var mediaQuery = MediaQuery.of(context);
|
||||
var screenSize = mediaQuery.size;
|
||||
var dpr = mediaQuery.devicePixelRatio;
|
||||
var requestWidth = imageWidth * dpr;
|
||||
var requestHeight = imageHeight * dpr;
|
||||
requestWidth = imageWidth * dpr;
|
||||
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(
|
||||
future: loader,
|
||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||
var ready = snapshot.connectionState == ConnectionState.done && !snapshot.hasError;
|
||||
return Hero(
|
||||
return MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
// remove bottom view insets to paint underneath the translucent navigation bar
|
||||
removeBottom: true,
|
||||
child: Scaffold(
|
||||
body: Hero(
|
||||
tag: uri,
|
||||
child: Stack(
|
||||
children: [
|
||||
|
@ -62,21 +64,26 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
|||
? CircularProgressIndicator()
|
||||
: Image.memory(
|
||||
widget.thumbnail,
|
||||
width: imageWidth.toDouble(),
|
||||
height: imageHeight.toDouble(),
|
||||
width: requestWidth,
|
||||
height: requestHeight,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
if (ready)
|
||||
Image.memory(
|
||||
snapshot.data,
|
||||
width: imageWidth.toDouble(),
|
||||
height: imageHeight.toDouble(),
|
||||
Center(
|
||||
child: FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image: FileImage(File(path)),
|
||||
fadeOutDuration: Duration(milliseconds: 1),
|
||||
fadeInDuration: Duration(milliseconds: 200),
|
||||
width: requestWidth,
|
||||
height: requestHeight,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue