Improve media resizing (#722)

* improve MediaUtils.getImageThumbnail so it does not load the whole bitmap into memory

* load thumbnails in device specific sizes
This commit is contained in:
Konrad Pozniak 2018-07-23 21:55:09 +02:00 committed by GitHub
parent 67f4479e86
commit 61f3f6c928
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 73 additions and 63 deletions

View File

@ -154,8 +154,6 @@ public final class ComposeActivity
private static final int MEDIA_PICK_RESULT = 1; private static final int MEDIA_PICK_RESULT = 1;
private static final int MEDIA_TAKE_PHOTO_RESULT = 2; private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1; private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
@Px
private static final int THUMBNAIL_SIZE = 128;
private static final String SAVED_TOOT_UID_EXTRA = "saved_toot_uid"; private static final String SAVED_TOOT_UID_EXTRA = "saved_toot_uid";
private static final String SAVED_TOOT_TEXT_EXTRA = "saved_toot_text"; private static final String SAVED_TOOT_TEXT_EXTRA = "saved_toot_text";
@ -210,6 +208,7 @@ public final class ComposeActivity
private int savedTootUid = 0; private int savedTootUid = 0;
private List<Emoji> emojiList; private List<Emoji> emojiList;
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT; private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
private @Px int thumbnailViewSize;
private SaveTootHelper saveTootHelper; private SaveTootHelper saveTootHelper;
@ -341,6 +340,8 @@ public final class ComposeActivity
actionPhotoTake.setOnClickListener(v -> initiateCameraApp()); actionPhotoTake.setOnClickListener(v -> initiateCameraApp());
actionPhotoPick.setOnClickListener(v -> onMediaPick()); actionPhotoPick.setOnClickListener(v -> onMediaPick());
thumbnailViewSize = getResources().getDimensionPixelSize(R.dimen.compose_media_preview_size);
/* Initialise all the state, or restore it from a previous run, to determine a "starting" /* Initialise all the state, or restore it from a previous run, to determine a "starting"
* state. */ * state. */
Status.Visibility startingVisibility = Status.Visibility.UNKNOWN; Status.Visibility startingVisibility = Status.Visibility.UNKNOWN;
@ -527,7 +528,7 @@ public final class ComposeActivity
} }
} else if (savedMediaQueued != null) { } else if (savedMediaQueued != null) {
for (SavedQueuedMedia item : savedMediaQueued) { for (SavedQueuedMedia item : savedMediaQueued) {
Bitmap preview = MediaUtils.getImageThumbnail(getContentResolver(), item.uri, THUMBNAIL_SIZE); Bitmap preview = MediaUtils.getImageThumbnail(getContentResolver(), item.uri, thumbnailViewSize);
addMediaToQueue(item.id, item.type, preview, item.uri, item.mediaSize, item.readyStage, item.description); addMediaToQueue(item.id, item.type, preview, item.uri, item.mediaSize, item.readyStage, item.description);
} }
} else if (intent != null && savedInstanceState == null) { } else if (intent != null && savedInstanceState == null) {
@ -1033,11 +1034,10 @@ public final class ComposeActivity
item.readyStage = readyStage; item.readyStage = readyStage;
ImageView view = item.preview; ImageView view = item.preview;
Resources resources = getResources(); Resources resources = getResources();
int side = resources.getDimensionPixelSize(R.dimen.compose_media_preview_side);
int margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin); int margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin);
int marginBottom = resources.getDimensionPixelSize( int marginBottom = resources.getDimensionPixelSize(
R.dimen.compose_media_preview_margin_bottom); R.dimen.compose_media_preview_margin_bottom);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(side, side); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize);
layoutParams.setMargins(margin, 0, margin, marginBottom); layoutParams.setMargins(margin, 0, margin, marginBottom);
view.setLayoutParams(layoutParams); view.setLayoutParams(layoutParams);
view.setScaleType(ImageView.ScaleType.CENTER_CROP); view.setScaleType(ImageView.ScaleType.CENTER_CROP);
@ -1343,7 +1343,7 @@ public final class ComposeActivity
displayTransientError(R.string.error_media_upload_image_or_video); displayTransientError(R.string.error_media_upload_image_or_video);
return; return;
} }
Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, THUMBNAIL_SIZE); Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, thumbnailViewSize);
if (bitmap != null) { if (bitmap != null) {
addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize); addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize);
} else { } else {
@ -1352,7 +1352,7 @@ public final class ComposeActivity
break; break;
} }
case "image": { case "image": {
Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, THUMBNAIL_SIZE); Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, thumbnailViewSize);
if (bitmap != null) { if (bitmap != null) {
addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize); addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize);
} else { } else {

View File

@ -36,6 +36,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.IOUtils import com.keylesspalace.tusky.util.IOUtils
import com.keylesspalace.tusky.util.MediaUtils
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.theartofdev.edmodo.cropper.CropImage import com.theartofdev.edmodo.cropper.CropImage
import kotlinx.android.synthetic.main.activity_edit_profile.* import kotlinx.android.synthetic.main.activity_edit_profile.*
@ -446,23 +447,9 @@ class EditProfileActivity : BaseActivity(), Injectable {
override fun doInBackground(vararg uris: Uri): Boolean? { override fun doInBackground(vararg uris: Uri): Boolean? {
val uri = uris[0] val uri = uris[0]
val inputStream: InputStream?
try {
inputStream = contentResolver.openInputStream(uri)
} catch (e: FileNotFoundException) {
Log.d(TAG, Log.getStackTraceString(e))
return false
}
val sourceBitmap: Bitmap? val sourceBitmap = MediaUtils.getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight)
try {
sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null)
} catch (error: OutOfMemoryError) {
Log.d(TAG, Log.getStackTraceString(error))
return false
} finally {
IOUtils.closeQuietly(inputStream)
}
if (sourceBitmap == null) { if (sourceBitmap == null) {
return false return false
} }

View File

@ -131,21 +131,6 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
return orientation; return orientation;
} }
private static int calculateInSampleSize(int width, int height, int requiredScale) {
int inSampleSize = 1;
if (height > requiredScale || width > requiredScale) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
/* Calculate the largest inSampleSize value that is a power of 2 and keeps both height
* and width larger than the requested height and width. */
while (halfHeight / inSampleSize >= requiredScale
&& halfWidth / inSampleSize >= requiredScale) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
@Override @Override
protected Boolean doInBackground(Uri... uris) { protected Boolean doInBackground(Uri... uris) {
resultList = new ArrayList<>(); resultList = new ArrayList<>();
@ -160,8 +145,6 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options); BitmapFactory.decodeStream(inputStream, null, options);
int beforeWidth = options.outWidth;
int beforeHeight = options.outHeight;
IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(inputStream);
// Get EXIF data, for orientation info. // Get EXIF data, for orientation info.
int orientation = getOrientation(uri, contentResolver); int orientation = getOrientation(uri, contentResolver);
@ -180,8 +163,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
return false; return false;
} }
options.inSampleSize = calculateInSampleSize(beforeWidth, beforeHeight, options.inSampleSize = MediaUtils.calculateInSampleSize(options, scaledImageSize, scaledImageSize);
scaledImageSize);
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
Bitmap scaledBitmap; Bitmap scaledBitmap;
try { try {

View File

@ -27,6 +27,7 @@ import android.provider.OpenableColumns;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.Px; import android.support.annotation.Px;
import android.util.Log;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -34,11 +35,10 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
/** /**
* Class who will have all the code link with Media * Class with helper methods for obtaining and resizing media files
* <p>
* Motivation : try to keep the ComposeActivity "smaller" and make modular method
*/ */
public class MediaUtils { public class MediaUtils {
private static final String TAG = "MediaUtils";
public static final int MEDIA_SIZE_UNKNOWN = -1; public static final int MEDIA_SIZE_UNKNOWN = -1;
/** /**
@ -88,30 +88,51 @@ public class MediaUtils {
} }
@Nullable @Nullable
public static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri, public static Bitmap getSampledBitmap(ContentResolver contentResolver, Uri uri, @Px int reqWidth, @Px int reqHeight) {
@Px int thumbnailSize) { // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
InputStream stream; InputStream stream;
try { try {
stream = contentResolver.openInputStream(uri); stream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.w(TAG, e);
return null; return null;
} }
Bitmap source = BitmapFactory.decodeStream(stream);
if (source == null) { BitmapFactory.decodeStream(stream, null, options);
IOUtils.closeQuietly(stream); IOUtils.closeQuietly(stream);
return null;
} // Calculate inSampleSize
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
source.recycle();
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
try { try {
if (stream != null) { stream = contentResolver.openInputStream(uri);
stream.close(); return BitmapFactory.decodeStream(stream, null, options);
} catch (FileNotFoundException e) {
Log.w(TAG, e);
return null;
} catch (OutOfMemoryError e) {
Log.e(TAG, "OutOfMemoryError while trying to get sampled Bitmap", e);
return null;
} finally {
IOUtils.closeQuietly(stream);
} }
} catch (IOException e) {
bitmap.recycle(); }
@Nullable
public static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri,
@Px int thumbnailSize) {
Bitmap source = getSampledBitmap(contentResolver, uri, thumbnailSize, thumbnailSize);
if(source != null) {
return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
} else {
return null; return null;
} }
return bitmap;
} }
@Nullable @Nullable
@ -128,8 +149,7 @@ public class MediaUtils {
} }
public static long getImageSquarePixels(ContentResolver contentResolver, Uri uri) throws FileNotFoundException { public static long getImageSquarePixels(ContentResolver contentResolver, Uri uri) throws FileNotFoundException {
InputStream input; InputStream input = contentResolver.openInputStream(uri);
input = contentResolver.openInputStream(uri);
final BitmapFactory.Options options = new BitmapFactory.Options(); final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
@ -139,4 +159,25 @@ public class MediaUtils {
return (long) options.outWidth * options.outHeight; return (long) options.outWidth * options.outHeight;
} }
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
} }

View File

@ -7,7 +7,7 @@
<dimen name="status_detail_media_preview_height">130dp</dimen> <dimen name="status_detail_media_preview_height">130dp</dimen>
<dimen name="compose_media_preview_margin">8dp</dimen> <dimen name="compose_media_preview_margin">8dp</dimen>
<dimen name="compose_media_preview_margin_bottom">0dp</dimen> <dimen name="compose_media_preview_margin_bottom">0dp</dimen>
<dimen name="compose_media_preview_side">120dp</dimen> <dimen name="compose_media_preview_size">120dp</dimen>
<dimen name="compose_options_margin">8dp</dimen> <dimen name="compose_options_margin">8dp</dimen>
<dimen name="account_avatar_margin">14dp</dimen> <dimen name="account_avatar_margin">14dp</dimen>
<dimen name="tab_page_margin">8dp</dimen> <dimen name="tab_page_margin">8dp</dimen>