WOA-3924 - (Performance) Image uploading in feed post and chat is slo… (#45)

* WOA-3924 - (Performance) Image uploading in feed post and chat is slow - Added resizeImage functionality in Android

* Change resolution to be exactly like on iOS
This commit is contained in:
Lev Vidrak 2017-06-26 10:09:32 +03:00 committed by Yedidya Kennard
parent 7a71a9c0d7
commit 6295dd6c0f
4 changed files with 275 additions and 49 deletions

View File

@ -1,18 +1,36 @@
package com.wix.RNCameraKit;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Base64;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import javax.annotation.Nullable;
public class Utils {
public final static String CONTENT_PREFIX = "content://";
public final static String FILE_PREFIX = "file://";
private static final int MAX_SAMPLE_SIZE = 8;
public static String getBase64FromBitmap(Bitmap bitmap) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
@ -49,4 +67,156 @@ public class Utils {
}
return list;
}
@NonNull
public static WritableMap resizeImage(Context context, String imageName, String imageUrlString, int maxResolution, int compressionQuality) throws IOException {
Bitmap sourceImage = null;
sourceImage = Utils.loadBitmapFromFile(context, imageUrlString, maxResolution, maxResolution);
if (sourceImage == null) {
throw new IOException("Unable to load source image from path");
}
Bitmap scaledImage = Utils.resizeImage(sourceImage, maxResolution, maxResolution);
if (sourceImage != scaledImage) {
sourceImage.recycle();
}
// Save the resulting image
File path = context.getCacheDir();
String resizedImagePath = Utils.saveImage(scaledImage, path, Long.toString(new Date().getTime()), Bitmap.CompressFormat.JPEG, compressionQuality);
// Clean up remaining image
scaledImage.recycle();
WritableMap ans = Arguments.createMap();
ans.putString("uri", FILE_PREFIX+resizedImagePath);
ans.putString("name", imageName);
ans.putInt("size", (int)new File(resizedImagePath).length());
ans.putInt("width", scaledImage.getWidth());
ans.putInt("height", scaledImage.getHeight());
return ans;
}
/**
* Resize the specified bitmap, keeping its aspect ratio.
*/
public static Bitmap resizeImage(Bitmap image, int maxWidth, int maxHeight) {
Bitmap newImage = null;
if (image == null) {
return null; // Can't load the image from the given path.
}
if (maxHeight > 0 && maxWidth > 0) {
float width = image.getWidth();
float height = image.getHeight();
float ratio = Math.min((float)maxWidth / width, (float)maxHeight / height);
int finalWidth = (int) (width * ratio);
int finalHeight = (int) (height * ratio);
newImage = Bitmap.createScaledBitmap(image, finalWidth, finalHeight, true);
}
return newImage;
}
/**
* Compute the inSampleSize value to use to load a bitmap.
* Adapted from https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
*/
public static int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) {
if (reqHeight == 0 || reqWidth == 0) {
return 1;
}
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while (inSampleSize <= MAX_SAMPLE_SIZE
&& (halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static Bitmap loadBitmap(Context context, String imagePath, BitmapFactory.Options options) throws IOException {
Bitmap sourceImage = null;
if (!imagePath.startsWith(CONTENT_PREFIX)) {
try {
sourceImage = BitmapFactory.decodeFile(imagePath, options);
} catch (Exception e) {
e.printStackTrace();
throw new IOException("Error decoding image file");
}
} else {
ContentResolver cr = context.getContentResolver();
InputStream input = cr.openInputStream(Uri.parse(imagePath));
if (input != null) {
sourceImage = BitmapFactory.decodeStream(input, null, options);
input.close();
}
}
return sourceImage;
}
/**
* Loads the bitmap resource from the file specified in imagePath.
*/
public static Bitmap loadBitmapFromFile(Context context, String imagePath, int newWidth,
int newHeight) throws IOException {
// Decode the image bounds to find the size of the source image.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
loadBitmap(context, imagePath, options);
// Set a sample size according to the image size to lower memory usage.
options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight , newWidth, newHeight);
options.inJustDecodeBounds = false;
return loadBitmap(context, imagePath, options);
}
/**
* Save the given bitmap in a directory. Extension is automatically generated using the bitmap format.
*/
public static String saveImage(Bitmap bitmap, File saveDirectory, String fileName,
Bitmap.CompressFormat compressFormat, int quality)
throws IOException {
if (bitmap == null) {
throw new IOException("The bitmap couldn't be resized");
}
File newFile = new File(saveDirectory, fileName + "." + compressFormat.name());
if(!newFile.createNewFile()) {
throw new IOException("The file already exists");
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(compressFormat, quality, outputStream);
byte[] bitmapData = outputStream.toByteArray();
outputStream.flush();
outputStream.close();
FileOutputStream fos = new FileOutputStream(newFile);
fos.write(bitmapData);
fos.flush();
fos.close();
return newFile.getAbsolutePath();
}
}

View File

@ -1,10 +1,14 @@
package com.wix.RNCameraKit.gallery;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
@ -13,34 +17,59 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.wix.RNCameraKit.SaveImageTask;
import com.wix.RNCameraKit.Utils;
import com.wix.RNCameraKit.gallery.permission.StoragePermission;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import static com.wix.RNCameraKit.Utils.getStringSafe;
/**
* Created by yedidyak on 29/06/2016.
*/
public class NativeGalleryModule extends ReactContextBaseJavaModule {
private final String IMAGE_URI_KEY = "uri";
private final String IMAGE_NAME_KEY = "name";
private final int HIGHE_DIMANTION = 1200;
private final int MEDIUM_DIMANTION = 800;
private final int LOW_DIMANTION = 600;
public static final int MEDIUM_COMPRESSION_QUALITY = 85;
public static final int HIGH_COMPRESSION_QUALITY = 92;
private final String HIGH_QUALITY = "high";
private final String MEDIUM_QUALITY = "medium";
private final String LOW_QUALITY = "low";
public static final String[] ALBUMS_PROJECTION = new String[]{
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
};
public static final String[] IMAGES_PROJECTION = new String[]{
MediaStore.Images.Media. _ID,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.TITLE,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.DATA
MediaStore.Images.Media._ID,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.TITLE,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.DATA
};
public static final String ALL_PHOTOS = "All Photos";
private Promise checkPermissionStatusPromise;
@ -62,8 +91,7 @@ public class NativeGalleryModule extends ReactContextBaseJavaModule {
public void addAlbum(String name, String uri) {
if (!albums.containsKey(name)) {
albums.put(name, new Album(name, uri));
}
else {
} else {
albums.get(name).count++;
}
}
@ -85,7 +113,7 @@ public class NativeGalleryModule extends ReactContextBaseJavaModule {
getReactApplicationContext().addLifecycleEventListener(new LifecycleEventListener() {
@Override
public void onHostResume() {
if (checkPermissionStatusPromise != null && getCurrentActivity() != null) {
if (checkPermissionStatusPromise != null && getCurrentActivity() != null) {
getCurrentActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
@ -142,10 +170,10 @@ public class NativeGalleryModule extends ReactContextBaseJavaModule {
private Bitmap getThumbnail(int thumbId) {
return MediaStore.Images.Thumbnails.getThumbnail(
getReactApplicationContext().getContentResolver(),
thumbId,
MediaStore.Images.Thumbnails.MINI_KIND,
null);
getReactApplicationContext().getContentResolver(),
thumbId,
MediaStore.Images.Thumbnails.MINI_KIND,
null);
}
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
@ -173,7 +201,6 @@ public class NativeGalleryModule extends ReactContextBaseJavaModule {
@ReactMethod
public void getAlbumsWithThumbnails(Promise promise) {
Cursor imagesCursor = getReactApplicationContext().getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ALBUMS_PROJECTION, null, null, null);
AlbumList albums = getAlbumListFromCursor(imagesCursor);
@ -189,16 +216,55 @@ public class NativeGalleryModule extends ReactContextBaseJavaModule {
promise.resolve(ret);
}
@ReactMethod
public void resizeImage(ReadableMap image, String quality, Promise promise) throws IOException {
try {
String imageName = Utils.getStringSafe(image, IMAGE_NAME_KEY);
String imageUrlString = getStringSafe(image, IMAGE_URI_KEY);
if (imageUrlString.startsWith(Utils.FILE_PREFIX)) {
imageUrlString = imageUrlString.replaceFirst(Utils.FILE_PREFIX, "");
}
// decide what is the wanted compression & resolution
int maxResolution;
int compressionQuality;
switch(quality) {
case HIGH_QUALITY:
maxResolution = HIGHE_DIMANTION;
compressionQuality = MEDIUM_COMPRESSION_QUALITY;
break;
case MEDIUM_QUALITY:
maxResolution = MEDIUM_DIMANTION;
compressionQuality = MEDIUM_COMPRESSION_QUALITY;
break;
case LOW_QUALITY:
maxResolution = LOW_DIMANTION;
compressionQuality = MEDIUM_COMPRESSION_QUALITY;
break;
default:
maxResolution = HIGHE_DIMANTION;
compressionQuality = HIGH_COMPRESSION_QUALITY;
}
WritableMap ans = Utils.resizeImage(getReactApplicationContext(),imageName, imageUrlString, maxResolution, compressionQuality );
promise.resolve(ans);
} catch (IOException e) {
Log.d("","Failed resize image e: "+e.getMessage());
}
}
@ReactMethod
public void getImagesForUris(ReadableArray uris, Promise promise) {
StringBuilder builder = new StringBuilder();
builder.append(MediaStore.Images.Media.DATA + " IN (");
for (int i=0; i<uris.size(); i++) {
for (int i = 0; i < uris.size(); i++) {
builder.append("\"");
builder.append(uris.getString(i));
builder.append("\"");
if(i != uris.size() -1) {
if (i != uris.size() - 1) {
builder.append(", ");
}
}
@ -243,19 +309,19 @@ public class NativeGalleryModule extends ReactContextBaseJavaModule {
@ReactMethod
public void saveImageURLToCameraRoll(String imageUrl, final Promise promise) {
new SaveImageTask(imageUrl, getReactApplicationContext(), promise, true).execute();
new SaveImageTask(imageUrl, getReactApplicationContext(), promise, true).execute();
}
@ReactMethod
public void deleteTempImage(String imageUrl, final Promise promise) {
boolean success = true;
String imagePath = imageUrl.replace("file://","");
String imagePath = imageUrl.replace("file://", "");
File imageFile = new File(imagePath);
if (imageFile.exists()) {
success = imageFile.delete();
}
if(promise != null) {
if (promise != null) {
WritableMap result = Arguments.createMap();
result.putBoolean("success", success);
promise.resolve(result);

View File

@ -15,6 +15,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import com.facebook.react.bridge.ReactContext;
import com.wix.RNCameraKit.Utils;
import java.util.concurrent.ThreadPoolExecutor;
@ -24,7 +25,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
public class SelectableImage extends FrameLayout {
private static final int MINI_THUMB_HEIGHT = 512;
private static final int MINI_THUMB_WIDTH = 384;
private static final int MAX_SAMPLE_SIZE = 8;
private static final int DEFAULT_SELECTED_IMAGE_GRAVITY = Gravity.TOP | Gravity.RIGHT;
public static final int SELECTED_IMAGE_NORMAL_SIZE_DP = 22;
@ -90,9 +91,10 @@ public class SelectableImage extends FrameLayout {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.inSampleSize = calculateInSampleSize(w, h);
this.inSampleSize = Utils.calculateInSampleSize(MINI_THUMB_WIDTH,MINI_THUMB_HEIGHT,w, h);
}
public void setScaleType(ImageView.ScaleType scaleType) {
imageView.setScaleType(scaleType);
}
@ -114,7 +116,7 @@ public class SelectableImage extends FrameLayout {
BitmapFactory.Options options = new BitmapFactory.Options();
if (inSampleSize == 0) {
inSampleSize = calculateInSampleSize(getWidth(), getHeight());
inSampleSize = Utils.calculateInSampleSize(MINI_THUMB_WIDTH,MINI_THUMB_HEIGHT, getWidth(), getHeight());
}
options.inSampleSize = inSampleSize;
@ -157,28 +159,6 @@ public class SelectableImage extends FrameLayout {
this.unselectedDrawable = unselectedDrawable;
}
public static int calculateInSampleSize(int reqWidth, int reqHeight) {
if (reqHeight == 0 || reqWidth == 0) {
return 1;
}
int inSampleSize = 1;
if (MINI_THUMB_HEIGHT > reqHeight || MINI_THUMB_WIDTH > reqWidth) {
final int halfHeight = MINI_THUMB_HEIGHT / 2;
final int halfWidth = MINI_THUMB_WIDTH / 2;
while (inSampleSize <= MAX_SAMPLE_SIZE
&& (halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
private LayoutParams createSelectedImageParams(Integer gravity, Integer sizeDp) {
gravity = gravity != null ? gravity : DEFAULT_SELECTED_IMAGE_GRAVITY;

View File

@ -51,6 +51,15 @@ async function requestDevicePhotosAuthorization() {
return isAuthorized;
}
async function resizeImage(image = {}, quality = 'original') {
if (quality === 'original') {
return images;
}
const ans = await NativeGalleryModule.resizeImage(image, quality);
return ans;
}
export default {
checkDevicePhotosAuthorizationStatus,
requestDevicePhotosAuthorization,
@ -58,5 +67,6 @@ export default {
getImageUriForId,
getImagesForIds,
getImageForTapEvent,
getImagesForCameraEvent
getImagesForCameraEvent,
resizeImage
}