mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-23 17:48:50 +00:00
Render images in a conversation true-to-size.
Previously, we were always rendering images as squares. Instead of doing that, we now render them as close to true-to-size as possible (within reasonable min/max width/height boundaries).
This commit is contained in:

committed by
Moxie Marlinspike

parent
9f8b4cf892
commit
ea374735e1
@@ -5,20 +5,26 @@ import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
@@ -26,11 +32,19 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
|
||||
public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private static final String TAG = ThumbnailView.class.getSimpleName();
|
||||
private static final int WIDTH = 0;
|
||||
private static final int HEIGHT = 1;
|
||||
private static final int MIN_WIDTH = 0;
|
||||
private static final int MAX_WIDTH = 1;
|
||||
private static final int MIN_HEIGHT = 2;
|
||||
private static final int MAX_HEIGHT = 3;
|
||||
|
||||
private ImageView image;
|
||||
private ImageView playOverlay;
|
||||
@@ -38,6 +52,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
private int radius;
|
||||
private OnClickListener parentClickListener;
|
||||
|
||||
private final int[] dimens = new int[2];
|
||||
private final int[] bounds = new int[4];
|
||||
private final int[] measureDimens = new int[2];
|
||||
|
||||
private Optional<TransferControlView> transferControls = Optional.absent();
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlideClickListener downloadClickListener = null;
|
||||
@@ -63,11 +81,110 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0);
|
||||
backgroundColorHint = typedArray.getColor(R.styleable.ThumbnailView_backgroundColorHint, Color.BLACK);
|
||||
backgroundColorHint = typedArray.getColor(R.styleable.ThumbnailView_backgroundColorHint, Color.BLACK);
|
||||
bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0);
|
||||
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
|
||||
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
|
||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasureSpec) {
|
||||
fillTargetDimensions(measureDimens, dimens, bounds);
|
||||
if (measureDimens[WIDTH] == 0 && measureDimens[HEIGHT] == 0) {
|
||||
super.onMeasure(originalWidthMeasureSpec, originalHeightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight();
|
||||
int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom();
|
||||
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
||||
int dimensFilledCount = getNonZeroCount(dimens);
|
||||
int boundsFilledCount = getNonZeroCount(bounds);
|
||||
|
||||
if (dimensFilledCount == 0 || boundsFilledCount == 0) {
|
||||
targetDimens[WIDTH] = 0;
|
||||
targetDimens[HEIGHT] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double naturalWidth = dimens[WIDTH];
|
||||
double naturalHeight = dimens[HEIGHT];
|
||||
|
||||
int minWidth = bounds[MIN_WIDTH];
|
||||
int maxWidth = bounds[MAX_WIDTH];
|
||||
int minHeight = bounds[MIN_HEIGHT];
|
||||
int maxHeight = bounds[MAX_HEIGHT];
|
||||
|
||||
if (dimensFilledCount > 0 && dimensFilledCount < dimens.length) {
|
||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %f x %f",
|
||||
naturalWidth, naturalHeight));
|
||||
}
|
||||
if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) {
|
||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]",
|
||||
minWidth, maxWidth, minHeight, maxHeight));
|
||||
}
|
||||
|
||||
double measuredWidth = naturalWidth;
|
||||
double measuredHeight = naturalHeight;
|
||||
|
||||
boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth;
|
||||
boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight;
|
||||
|
||||
if (!widthInBounds || !heightInBounds) {
|
||||
double minWidthRatio = naturalWidth / minWidth;
|
||||
double maxWidthRatio = naturalWidth / maxWidth;
|
||||
double minHeightRatio = naturalHeight / minHeight;
|
||||
double maxHeightRatio = naturalHeight / maxHeight;
|
||||
|
||||
if (maxWidthRatio > 1 || maxHeightRatio > 1) {
|
||||
if (maxWidthRatio >= maxHeightRatio) {
|
||||
measuredWidth /= maxWidthRatio;
|
||||
measuredHeight /= maxWidthRatio;
|
||||
} else {
|
||||
measuredWidth /= maxHeightRatio;
|
||||
measuredHeight /= maxHeightRatio;
|
||||
}
|
||||
|
||||
measuredWidth = Math.max(measuredWidth, minWidth);
|
||||
measuredHeight = Math.max(measuredHeight, minHeight);
|
||||
|
||||
} else if (minWidthRatio < 1 || minHeightRatio < 1) {
|
||||
if (minWidthRatio <= minHeightRatio) {
|
||||
measuredWidth /= minWidthRatio;
|
||||
measuredHeight /= minWidthRatio;
|
||||
} else {
|
||||
measuredWidth /= minHeightRatio;
|
||||
measuredHeight /= minHeightRatio;
|
||||
}
|
||||
|
||||
measuredWidth = Math.min(measuredWidth, maxWidth);
|
||||
measuredHeight = Math.min(measuredHeight, maxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
targetDimens[WIDTH] = (int) measuredWidth;
|
||||
targetDimens[HEIGHT] = (int) measuredHeight;
|
||||
}
|
||||
|
||||
private int getNonZeroCount(int[] vals) {
|
||||
int count = 0;
|
||||
for (int val : vals) {
|
||||
if (val > 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener l) {
|
||||
parentClickListener = l;
|
||||
@@ -96,9 +213,22 @@ public class ThumbnailView extends FrameLayout {
|
||||
this.backgroundColorHint = color;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
|
||||
boolean showControls, boolean isPreview)
|
||||
{
|
||||
setImageResource(glideRequests, slide, showControls, isPreview, 0, 0);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
|
||||
boolean showControls, boolean isPreview, int naturalWidth,
|
||||
int naturalHeight)
|
||||
{
|
||||
dimens[WIDTH] = naturalWidth;
|
||||
dimens[HEIGHT] = naturalHeight;
|
||||
invalidate();
|
||||
|
||||
if (showControls) {
|
||||
getTransferControls().setSlide(slide);
|
||||
getTransferControls().setDownloadClickListener(new DownloadClickDispatcher());
|
||||
@@ -136,11 +266,11 @@ public class ThumbnailView extends FrameLayout {
|
||||
if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(glideRequests, slide).into(image);
|
||||
else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(glideRequests, slide).into(image);
|
||||
else glideRequests.clear(image);
|
||||
|
||||
}
|
||||
|
||||
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
|
||||
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
|
||||
|
||||
glideRequests.load(new DecryptableUri(uri))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.transform(new RoundedCorners(radius))
|
||||
@@ -171,22 +301,29 @@ public class ThumbnailView extends FrameLayout {
|
||||
getTransferControls().showProgressSpinner();
|
||||
}
|
||||
|
||||
private RequestBuilder buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
RequestBuilder builder = glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
||||
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.transform(new RoundedCorners(radius))
|
||||
.centerCrop()
|
||||
.transition(withCrossFade());
|
||||
.transition(withCrossFade()), new CenterCrop());
|
||||
|
||||
if (slide.isInProgress()) return builder;
|
||||
else return builder.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
if (slide.isInProgress()) return request;
|
||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
}
|
||||
|
||||
private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
return glideRequests.asBitmap()
|
||||
return applySizing(glideRequests.asBitmap()
|
||||
.load(slide.getPlaceholderRes(getContext().getTheme()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.fitCenter();
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE), new FitCenter());
|
||||
}
|
||||
|
||||
private GlideRequest applySizing(@NonNull GlideRequest request, @NonNull BitmapTransformation unavailableDimensSizing) {
|
||||
int[] size = new int[2];
|
||||
fillTargetDimensions(size, dimens, bounds);
|
||||
if (size[WIDTH] == 0 && size[HEIGHT] == 0) {
|
||||
return request.transforms(unavailableDimensSizing, new RoundedCorners(radius));
|
||||
}
|
||||
return request.override(size[WIDTH], size[HEIGHT])
|
||||
.transforms(new CenterCrop(), new RoundedCorners(radius));
|
||||
}
|
||||
|
||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||
|
Reference in New Issue
Block a user