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:
parent
67f4479e86
commit
61f3f6c928
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue