Improve media resizing (#722)

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

* load thumbnails in device specific sizes
main
Konrad Pozniak 6 years ago committed by GitHub
parent de361dcb7a
commit 9c7ddc7d32
  1. 14
      app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
  2. 19
      app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt
  3. 20
      app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java
  4. 79
      app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java
  5. 2
      app/src/main/res/values/dimens.xml

@ -154,8 +154,6 @@ public final class ComposeActivity
private static final int MEDIA_PICK_RESULT = 1;
private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
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_TEXT_EXTRA = "saved_toot_text";
@ -210,6 +208,7 @@ public final class ComposeActivity
private int savedTootUid = 0;
private List<Emoji> emojiList;
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
private @Px int thumbnailViewSize;
private SaveTootHelper saveTootHelper;
@ -341,6 +340,8 @@ public final class ComposeActivity
actionPhotoTake.setOnClickListener(v -> initiateCameraApp());
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"
* state. */
Status.Visibility startingVisibility = Status.Visibility.UNKNOWN;
@ -527,7 +528,7 @@ public final class ComposeActivity
}
} else if (savedMediaQueued != null) {
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);
}
} else if (intent != null && savedInstanceState == null) {
@ -1033,11 +1034,10 @@ public final class ComposeActivity
item.readyStage = readyStage;
ImageView view = item.preview;
Resources resources = getResources();
int side = resources.getDimensionPixelSize(R.dimen.compose_media_preview_side);
int margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin);
int marginBottom = resources.getDimensionPixelSize(
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);
view.setLayoutParams(layoutParams);
view.setScaleType(ImageView.ScaleType.CENTER_CROP);
@ -1343,7 +1343,7 @@ public final class ComposeActivity
displayTransientError(R.string.error_media_upload_image_or_video);
return;
}
Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, THUMBNAIL_SIZE);
Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, thumbnailViewSize);
if (bitmap != null) {
addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize);
} else {
@ -1352,7 +1352,7 @@ public final class ComposeActivity
break;
}
case "image": {
Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, THUMBNAIL_SIZE);
Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, thumbnailViewSize);
if (bitmap != null) {
addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize);
} else {

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

@ -131,21 +131,6 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
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
protected Boolean doInBackground(Uri... uris) {
resultList = new ArrayList<>();
@ -160,8 +145,6 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
int beforeWidth = options.outWidth;
int beforeHeight = options.outHeight;
IOUtils.closeQuietly(inputStream);
// Get EXIF data, for orientation info.
int orientation = getOrientation(uri, contentResolver);
@ -180,8 +163,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
} catch (FileNotFoundException e) {
return false;
}
options.inSampleSize = calculateInSampleSize(beforeWidth, beforeHeight,
scaledImageSize);
options.inSampleSize = MediaUtils.calculateInSampleSize(options, scaledImageSize, scaledImageSize);
options.inJustDecodeBounds = false;
Bitmap scaledBitmap;
try {

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

@ -7,7 +7,7 @@
<dimen name="status_detail_media_preview_height">130dp</dimen>
<dimen name="compose_media_preview_margin">8dp</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="account_avatar_margin">14dp</dimen>
<dimen name="tab_page_margin">8dp</dimen>

Loading…
Cancel
Save