From 961fdc8e7276298a29959df3eaa82c72885e9bb4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 28 Nov 2018 17:01:32 -0200 Subject: [PATCH] [android] Show multiple images in message bubble --- .../conversation/ConversationAdapter.java | 7 +- .../ConversationMessageViewHolder.java | 28 ++-- .../android/conversation/ImageAdapter.java | 115 +++++++++++++--- .../conversation/ImageItemDecoration.java | 66 +++++++++ .../android/conversation/ImageViewHolder.java | 31 ++++- .../conversation/SingleImageViewHolder.java | 53 -------- .../glide/BriarImageTransformation.java | 6 +- .../glide/CustomCornersTransformation.java | 128 ++++++++++++++++++ .../glide/ImageCornerTransformation.java | 111 --------------- .../android/conversation/glide/Radii.java | 41 ++++++ .../list_item_conversation_msg_image.xml | 15 +- .../list_item_conversation_msg_image_text.xml | 13 +- .../layout/list_item_conversation_msg_in.xml | 8 +- .../layout/list_item_conversation_msg_out.xml | 8 +- .../src/main/res/layout/list_item_image.xml | 3 +- briar-android/src/main/res/values/dimens.xml | 2 +- 16 files changed, 408 insertions(+), 227 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageItemDecoration.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/SingleImageViewHolder.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/CustomCornersTransformation.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/Radii.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java index 21d68897b..0194a2d7f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java @@ -23,6 +23,7 @@ class ConversationAdapter private ConversationListener listener; private final RecycledViewPool imageViewPool; + private final ImageItemDecoration imageItemDecoration; ConversationAdapter(Context ctx, ConversationListener conversationListener) { @@ -30,6 +31,8 @@ class ConversationAdapter listener = conversationListener; // This shares the same pool for view recycling between all image lists imageViewPool = new RecycledViewPool(); + // Share the item decoration as well + imageItemDecoration = new ImageItemDecoration(ctx); } @LayoutRes @@ -47,10 +50,10 @@ class ConversationAdapter switch (type) { case R.layout.list_item_conversation_msg_in: return new ConversationMessageViewHolder(v, listener, true, - imageViewPool); + imageViewPool, imageItemDecoration); case R.layout.list_item_conversation_msg_out: return new ConversationMessageViewHolder(v, listener, false, - imageViewPool); + imageViewPool, imageItemDecoration); case R.layout.list_item_conversation_notice_in: return new ConversationNoticeViewHolder(v, listener, true); case R.layout.list_item_conversation_notice_out: diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index 13d7c533c..7893071aa 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -2,8 +2,6 @@ package org.briarproject.briar.android.conversation; import android.support.annotation.UiThread; import android.support.constraint.ConstraintSet; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.RecycledViewPool; import android.view.View; @@ -13,6 +11,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; import static android.support.constraint.ConstraintSet.WRAP_CONTENT; +import static android.support.v4.content.ContextCompat.getColor; @UiThread @NotNullByDefault @@ -26,21 +25,22 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { private final ConstraintSet imageTextConstraints = new ConstraintSet(); ConversationMessageViewHolder(View v, ConversationListener listener, - boolean isIncoming, RecycledViewPool imageViewPool) { + boolean isIncoming, RecycledViewPool imageViewPool, + ImageItemDecoration imageItemDecoration) { super(v, listener, isIncoming); statusLayout = v.findViewById(R.id.statusLayout); // image list - RecyclerView list = v.findViewById(R.id.imageView); + RecyclerView list = v.findViewById(R.id.imageList); list.setRecycledViewPool(imageViewPool); - list.setLayoutManager(new GridLayoutManager(v.getContext(), 2)); - adapter = new ImageAdapter(listener); + adapter = + new ImageAdapter(v.getContext(), imageItemDecoration, listener); list.setAdapter(adapter); + list.addItemDecoration(imageItemDecoration); // remember original status text color timeColor = time.getCurrentTextColor(); - timeColorBubble = - ContextCompat.getColor(v.getContext(), R.color.briar_white); + timeColorBubble = getColor(v.getContext(), R.color.briar_white); // clone constraint sets from layout files textConstraints @@ -80,8 +80,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { private void bindImageItem(ConversationMessageItem item) { ConstraintSet constraintSet; if (item.getText() == null) { - statusLayout - .setBackgroundResource(R.drawable.msg_status_bubble); + statusLayout.setBackgroundResource(R.drawable.msg_status_bubble); time.setTextColor(timeColorBubble); constraintSet = imageConstraints; } else { @@ -94,11 +93,12 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { AttachmentItem attachment = item.getAttachments().get(0); int width = attachment.getThumbnailWidth(); int height = attachment.getThumbnailHeight(); - constraintSet.constrainWidth(R.id.imageView, width); - constraintSet.constrainHeight(R.id.imageView, height); + constraintSet.constrainWidth(R.id.imageList, width); + constraintSet.constrainHeight(R.id.imageList, height); } else { - constraintSet.constrainWidth(R.id.imageView, WRAP_CONTENT); - constraintSet.constrainHeight(R.id.imageView, WRAP_CONTENT); + // bubble adapts to size of image list + constraintSet.constrainWidth(R.id.imageList, WRAP_CONTENT); + constraintSet.constrainHeight(R.id.imageList, WRAP_CONTENT); } constraintSet.applyTo(layout); adapter.setConversationItem(item); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java index 42e4991a7..134f19975 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java @@ -1,64 +1,72 @@ package org.briarproject.briar.android.conversation; +import android.content.Context; +import android.content.res.Resources; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView.Adapter; +import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.conversation.glide.Radii; import java.util.ArrayList; import java.util.List; +import static android.content.Context.WINDOW_SERVICE; import static java.util.Objects.requireNonNull; @NotNullByDefault class ImageAdapter extends Adapter { - private final static int TYPE_SINGLE = 0; - private final static int TYPE_MULTIPLE = 1; - private final List items = new ArrayList<>(); private final ConversationListener listener; + private final int imageSize, borderSize; + private final int radiusBig, radiusSmall; + private final boolean isRtl; @Nullable private ConversationMessageItem conversationItem; - public ImageAdapter(ConversationListener listener) { - super(); + public ImageAdapter(Context ctx, ImageItemDecoration imageItemDecoration, + ConversationListener listener) { this.listener = listener; - } - - @Override - public int getItemViewType(int position) { - return items.size() == 1 ? TYPE_SINGLE : TYPE_MULTIPLE; + borderSize = imageItemDecoration.getBorderSize(); + imageSize = getImageSize(ctx); + Resources res = ctx.getResources(); + radiusBig = + res.getDimensionPixelSize(R.dimen.message_bubble_radius_big); + radiusSmall = + res.getDimensionPixelSize(R.dimen.message_bubble_radius_small); + isRtl = imageItemDecoration.isRtl(); } @Override public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) { View v = LayoutInflater.from(viewGroup.getContext()).inflate( R.layout.list_item_image, viewGroup, false); - return type == TYPE_SINGLE ? new SingleImageViewHolder(v) : - new ImageViewHolder(v); + return new ImageViewHolder(v, imageSize, borderSize); } @Override public void onBindViewHolder(ImageViewHolder imageViewHolder, int position) { + // get item requireNonNull(conversationItem); AttachmentItem item = items.get(position); + // set onClick listener imageViewHolder.itemView.setOnClickListener(v -> listener.onAttachmentClicked(v, conversationItem, item) ); - if (imageViewHolder instanceof SingleImageViewHolder) { - boolean isIncoming = conversationItem.isIncoming(); - boolean hasText = conversationItem.getText() != null; - ((SingleImageViewHolder) imageViewHolder) - .bind(item, isIncoming, hasText); - } else { - imageViewHolder.bind(item); - } + // bind view holder + int size = items.size(); + boolean isIncoming = conversationItem.isIncoming(); + boolean hasText = conversationItem.getText() != null; + Radii r = getRadii(position, size, isIncoming, hasText); + imageViewHolder.bind(item, r, size == 1, singleInRow(position, size)); } @Override @@ -73,9 +81,76 @@ class ImageAdapter extends Adapter { notifyDataSetChanged(); } + private int getImageSize(Context ctx) { + Resources res = ctx.getResources(); + WindowManager windowManager = + (WindowManager) ctx.getSystemService(WINDOW_SERVICE); + DisplayMetrics displayMetrics = new DisplayMetrics(); + if (windowManager == null) { + return res.getDimensionPixelSize( + R.dimen.message_bubble_image_default); + } + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + int imageSize = displayMetrics.widthPixels / 3; + int maxSize = res.getDimensionPixelSize( + R.dimen.message_bubble_image_max_width); + return Math.min(imageSize, maxSize); + } + + private Radii getRadii(int pos, int num, boolean isIncoming, + boolean hasText) { + boolean left = isLeft(pos); + boolean single = num == 1; + // Top Row + int topLeft; + int topRight; + if (single) { + topLeft = isIncoming ? radiusSmall : radiusBig; + topRight = !isIncoming ? radiusSmall : radiusBig; + } else if (isTopRow(pos)) { + topLeft = left ? (isIncoming ? radiusSmall : radiusBig) : 0; + topRight = !left ? (!isIncoming ? radiusSmall : radiusBig) : 0; + } else { + topLeft = 0; + topRight = 0; + } + // Bottom Row + boolean singleInRow = singleInRow(pos, num); + int bottomLeft; + int bottomRight; + if (!hasText && isBottomRow(pos, num)) { + bottomLeft = singleInRow || left ? radiusBig : 0; + bottomRight = singleInRow || !left ? radiusBig : 0; + } else { + bottomLeft = 0; + bottomRight = 0; + } + if (isRtl) return new Radii(topRight, topLeft, bottomRight, bottomLeft); + return new Radii(topLeft, topRight, bottomLeft, bottomRight); + } + void clear() { items.clear(); notifyDataSetChanged(); } + static boolean isTopRow(int pos) { + return pos < 2; + } + + static boolean isLeft(int pos) { + return pos % 2 == 0; + } + + static boolean isBottomRow(int pos, int num) { + return num % 2 == 0 ? + pos >= num - 2 : // last two, if even + pos > num - 2; // last one, if odd + } + + static boolean singleInRow(int pos, int num) { + // last item of an odd number + return num % 2 != 0 && pos == num -1; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageItemDecoration.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageItemDecoration.java new file mode 100644 index 000000000..c01f962c8 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageItemDecoration.java @@ -0,0 +1,66 @@ +package org.briarproject.briar.android.conversation; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ItemDecoration; +import android.support.v7.widget.RecyclerView.State; +import android.view.View; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.R; + +import static android.os.Build.VERSION.SDK_INT; +import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL; +import static org.briarproject.briar.android.conversation.ImageAdapter.isBottomRow; +import static org.briarproject.briar.android.conversation.ImageAdapter.isLeft; +import static org.briarproject.briar.android.conversation.ImageAdapter.isTopRow; +import static org.briarproject.briar.android.conversation.ImageAdapter.singleInRow; + +@NotNullByDefault +class ImageItemDecoration extends ItemDecoration { + + private final int realBorderSize, border; + private final boolean isRtl; + + public ImageItemDecoration(Context ctx) { + Resources res = ctx.getResources(); + + // for pixel perfection, add a pixel to the border if it has an odd size + int b = res.getDimensionPixelSize(R.dimen.message_bubble_border); + realBorderSize = b % 2 == 0 ? b : b + 1;; + + // we are applying half the border around the insides of each image + // to prevent differently sized images looking slightly broken + border = realBorderSize / 2; + + // find out if we are showing a RTL language + Configuration config = res.getConfiguration(); + isRtl = SDK_INT >= 17 && + config.getLayoutDirection() == LAYOUT_DIRECTION_RTL; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + State state) { + if (state.getItemCount() == 1) return; + int pos = parent.getChildAdapterPosition(view); + int num = state.getItemCount(); + boolean left = isLeft(pos) ^ isRtl; + outRect.top = isTopRow(pos) ? 0 : border; + outRect.left = left ? 0 : border; + outRect.right = left && !singleInRow(pos, num) ? border : 0; + outRect.bottom = isBottomRow(pos, num) ? 0 : border; + } + + public int getBorderSize() { + return realBorderSize; + } + + public boolean isRtl() { + return isRtl; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java index 4c8985453..271fa52ea 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java @@ -3,15 +3,17 @@ package org.briarproject.briar.android.conversation; import android.graphics.Bitmap; import android.support.annotation.DrawableRes; import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams; import android.view.View; import android.widget.ImageView; import com.bumptech.glide.load.Transformation; -import com.bumptech.glide.load.resource.bitmap.CenterCrop; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.conversation.glide.BriarImageTransformation; import org.briarproject.briar.android.conversation.glide.GlideApp; +import org.briarproject.briar.android.conversation.glide.Radii; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; @@ -23,24 +25,41 @@ class ImageViewHolder extends ViewHolder { private static final int ERROR_RES = R.drawable.ic_image_broken; protected final ImageView imageView; - protected Transformation transformation = new CenterCrop(); + private final int imageSize, borderSize; - public ImageViewHolder(View v) { + public ImageViewHolder(View v, int imageSize, int borderSize) { super(v); imageView = v.findViewById(R.id.imageView); + this.imageSize = imageSize; + this.borderSize = borderSize; } - void bind(AttachmentItem attachment) { + void bind(AttachmentItem attachment, Radii r, boolean single, + boolean needsStretch) { if (attachment.hasError()) { GlideApp.with(imageView) .clear(imageView); imageView.setImageResource(ERROR_RES); } else { - loadImage(attachment); + setImageViewDimensions(attachment, single, needsStretch); + loadImage(attachment, r); } } - private void loadImage(AttachmentItem a) { + private void setImageViewDimensions(AttachmentItem a, boolean single, + boolean needsStretch) { + LayoutParams params = (LayoutParams) imageView.getLayoutParams(); + // actual image size will shrink half the border + int stretchSize = (imageSize - borderSize / 2) * 2 + borderSize; + int width = needsStretch ? stretchSize : imageSize; + params.width = single ? a.getThumbnailWidth() : width; + params.height = single ? a.getThumbnailHeight() : imageSize; + params.setFullSpan(!single && needsStretch); + imageView.setLayoutParams(params); + } + + private void loadImage(AttachmentItem a, Radii r) { + Transformation transformation = new BriarImageTransformation(r); GlideApp.with(imageView) .load(a) .diskCacheStrategy(NONE) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/SingleImageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/SingleImageViewHolder.java deleted file mode 100644 index f0f964e46..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/SingleImageViewHolder.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.briarproject.briar.android.conversation; - -import android.content.res.Configuration; -import android.view.View; -import android.view.ViewGroup.LayoutParams; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.R; -import org.briarproject.briar.android.conversation.glide.BriarImageTransformation; - -import static android.os.Build.VERSION.SDK_INT; -import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL; - -@NotNullByDefault -class SingleImageViewHolder extends ImageViewHolder { - - private final int radiusBig, radiusSmall; - private final boolean isRtl; - - public SingleImageViewHolder(View v) { - super(v); - radiusBig = v.getContext().getResources() - .getDimensionPixelSize(R.dimen.message_bubble_radius_big); - radiusSmall = v.getContext().getResources() - .getDimensionPixelSize(R.dimen.message_bubble_radius_small); - - // find out if we are showing a RTL language, Use the configuration, - // because getting the layout direction of views is not reliable - Configuration config = v.getContext().getResources().getConfiguration(); - isRtl = SDK_INT >= 17 && - config.getLayoutDirection() == LAYOUT_DIRECTION_RTL; - } - - void bind(AttachmentItem a, boolean isIncoming, boolean hasText) { - if (!a.hasError()) beforeLoadingImage(a, isIncoming, hasText); - super.bind(a); - } - - private void beforeLoadingImage(AttachmentItem a, boolean isIncoming, - boolean hasText) { - // apply image size constraints, so glides picks them up for scaling - LayoutParams layoutParams = - new LayoutParams(a.getThumbnailWidth(), a.getThumbnailHeight()); - imageView.setLayoutParams(layoutParams); - - boolean leftCornerSmall = - (isIncoming && !isRtl) || (!isIncoming && isRtl); - boolean bottomRound = !hasText; - transformation = new BriarImageTransformation(radiusSmall, radiusBig, - leftCornerSmall, bottomRound); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java index 5488efa40..c2bb1dbc0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java @@ -7,10 +7,8 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop; public class BriarImageTransformation extends MultiTransformation { - public BriarImageTransformation(int smallRadius, int radius, - boolean leftCornerSmall, boolean bottomRound) { - super(new CenterCrop(), new ImageCornerTransformation( - smallRadius, radius, leftCornerSmall, bottomRound)); + public BriarImageTransformation(Radii r) { + super(new CenterCrop(), new CustomCornersTransformation(r)); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/CustomCornersTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/CustomCornersTransformation.java new file mode 100644 index 000000000..fc013750c --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/CustomCornersTransformation.java @@ -0,0 +1,128 @@ +package org.briarproject.briar.android.conversation.glide; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.support.annotation.NonNull; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.security.MessageDigest; + +import javax.annotation.concurrent.Immutable; + +import static android.graphics.Bitmap.Config.ARGB_8888; +import static android.graphics.Shader.TileMode.CLAMP; + +@Immutable +@NotNullByDefault +public class CustomCornersTransformation extends BitmapTransformation { + + private static final String ID = CustomCornersTransformation.class.getName(); + + private final Radii radii; + + public CustomCornersTransformation(Radii radii) { + this.radii = radii; + } + + @Override + protected Bitmap transform(BitmapPool pool, Bitmap toTransform, + int outWidth, int outHeight) { + int width = toTransform.getWidth(); + int height = toTransform.getHeight(); + + Bitmap bitmap = pool.get(width, height, ARGB_8888); + bitmap.setHasAlpha(true); + + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP)); + drawRect(canvas, paint, width, height); + return bitmap; + } + + private void drawRect(Canvas canvas, Paint paint, float width, + float height) { + drawTopLeft(canvas, paint, radii.topLeft, width, height); + drawTopRight(canvas, paint, radii.topRight, width, height); + drawBottomLeft(canvas, paint, radii.bottomLeft, width, height); + drawBottomRight(canvas, paint, radii.bottomRight, width, height); + } + + private void drawTopLeft(Canvas canvas, Paint paint, int radius, + float width, float height) { + RectF rect = new RectF( + 0, + 0, + width / 2 + radius + 1, + height / 2 + radius + 1 + ); + if (radius == 0) canvas.drawRect(rect, paint); + else canvas.drawRoundRect(rect, radius, radius, paint); + } + + private void drawTopRight(Canvas canvas, Paint paint, int radius, + float width, float height) { + RectF rect = new RectF( + width / 2 - radius, + 0, + width, + height / 2 + radius + 1 + ); + if (radius == 0) canvas.drawRect(rect, paint); + else canvas.drawRoundRect(rect, radius, radius, paint); + } + + private void drawBottomLeft(Canvas canvas, Paint paint, int radius, + float width, float height) { + RectF rect = new RectF( + 0, + height / 2 - radius, + width / 2 + radius + 1, + height + ); + if (radius == 0) canvas.drawRect(rect, paint); + else canvas.drawRoundRect(rect, radius, radius, paint); + } + + private void drawBottomRight(Canvas canvas, Paint paint, int radius, + float width, float height) { + RectF rect = new RectF( + width / 2 - radius, + height / 2 - radius, + width, + height + ); + if (radius == 0) canvas.drawRect(rect, paint); + else canvas.drawRoundRect(rect, radius, radius, paint); + } + + @Override + public String toString() { + return "ImageCornerTransformation(" + radii + ")"; + } + + @Override + public boolean equals(Object o) { + return o instanceof CustomCornersTransformation && + radii.equals(((CustomCornersTransformation) o).radii); + } + + @Override + public int hashCode() { + return ID.hashCode() + radii.hashCode(); + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update((ID + radii).getBytes(CHARSET)); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java deleted file mode 100644 index b41c0cf31..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.briarproject.briar.android.conversation.glide; - -import android.graphics.Bitmap; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; -import android.support.annotation.NonNull; - -import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; -import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import java.security.MessageDigest; - -import javax.annotation.concurrent.Immutable; - -import static android.graphics.Bitmap.Config.ARGB_8888; -import static android.graphics.Shader.TileMode.CLAMP; - -@Immutable -@NotNullByDefault -class ImageCornerTransformation extends BitmapTransformation { - - private static final String ID = ImageCornerTransformation.class.getName(); - - private final int smallRadius, radius; - private final boolean leftCornerSmall, bottomRound; - - ImageCornerTransformation(int smallRadius, int radius, - boolean leftCornerSmall, boolean bottomRound) { - this.smallRadius = smallRadius; - this.radius = radius; - this.leftCornerSmall = leftCornerSmall; - this.bottomRound = bottomRound; - } - - @Override - protected Bitmap transform(BitmapPool pool, Bitmap toTransform, - int outWidth, int outHeight) { - int width = toTransform.getWidth(); - int height = toTransform.getHeight(); - - Bitmap bitmap = pool.get(width, height, ARGB_8888); - bitmap.setHasAlpha(true); - - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP)); - drawRect(canvas, paint, width, height); - return bitmap; - } - - private void drawRect(Canvas canvas, Paint paint, float width, - float height) { - drawSmallCorner(canvas, paint, width); - drawBigCorners(canvas, paint, width, height); - } - - private void drawSmallCorner(Canvas canvas, Paint paint, float width) { - float left = leftCornerSmall ? 0 : width - radius; - float right = leftCornerSmall ? radius : width; - canvas.drawRoundRect(new RectF(left, 0, right, radius), - smallRadius, smallRadius, paint); - } - - private void drawBigCorners(Canvas canvas, Paint paint, float width, - float height) { - float top = bottomRound ? 0 : radius; - RectF rect = new RectF(0, top, width, height); - if (bottomRound) { - canvas.drawRoundRect(rect, radius, radius, paint); - } else { - canvas.drawRect(rect, paint); - canvas.drawRoundRect(new RectF(0, 0, width, radius * 2), - radius, radius, paint); - } - } - - @Override - public String toString() { - return "ImageCornerTransformation(smallRadius=" + smallRadius + - ", radius=" + radius + ", leftCornerSmall=" + leftCornerSmall + - ", bottomRound=" + bottomRound + ")"; - } - - @Override - public boolean equals(Object o) { - return o instanceof ImageCornerTransformation && - ((ImageCornerTransformation) o).smallRadius == smallRadius && - ((ImageCornerTransformation) o).radius == radius && - ((ImageCornerTransformation) o).leftCornerSmall == - leftCornerSmall && - ((ImageCornerTransformation) o).bottomRound == bottomRound; - } - - @Override - public int hashCode() { - return ID.hashCode() + (smallRadius << 16) ^ (radius << 2) ^ - (leftCornerSmall ? 2 : 0) ^ (bottomRound ? 1 : 0); - } - - @Override - public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update((ID + '|' + smallRadius + '|' + radius + '|' + - leftCornerSmall + '|' + bottomRound).getBytes(CHARSET)); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/Radii.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/Radii.java new file mode 100644 index 000000000..318140ff0 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/Radii.java @@ -0,0 +1,41 @@ +package org.briarproject.briar.android.conversation.glide; + +import android.support.annotation.Nullable; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +public class Radii { + + public final int topLeft, topRight, bottomLeft, bottomRight; + + public Radii(int topLeft, int topRight, int bottomLeft, int bottomRight) { + this.topLeft = topLeft; + this.topRight = topRight; + this.bottomLeft = bottomLeft; + this.bottomRight = bottomRight; + } + + @Override + public boolean equals(@Nullable Object o) { + return o instanceof Radii && + topLeft == ((Radii) o).topLeft && + topRight == ((Radii) o).topRight && + bottomLeft == ((Radii) o).bottomLeft && + bottomRight == ((Radii) o).bottomRight; + } + + @Override + public int hashCode() { + return topLeft << 24 ^ topRight << 16 ^ bottomLeft << 8 ^ bottomRight; + } + + @Override + public String toString() { + return "Radii(topLeft=" + topLeft + + ",topRight=" + topRight + + ",bottomLeft=" + bottomLeft + + ",bottomRight=" + bottomRight; + } + +} diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml index e3e6d7917..c7459d36f 100644 --- a/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml @@ -16,15 +16,18 @@ android:elevation="@dimen/message_bubble_elevation"> + tools:listitem="@layout/list_item_image"/> diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml index 44d1bca42..e90bd0221 100644 --- a/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml @@ -16,15 +16,18 @@ android:elevation="@dimen/message_bubble_elevation"> + tools:listitem="@layout/list_item_image"/> @@ -41,7 +45,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/imageView" + app:layout_constraintTop_toBottomOf="@+id/imageList" tools:text="The text of a message which can sometimes be a bit longer as well"/> @@ -47,7 +51,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/imageView" + app:layout_constraintTop_toBottomOf="@+id/imageList" tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/> + tools:srcCompat="@tools:sample/avatars"/> diff --git a/briar-android/src/main/res/values/dimens.xml b/briar-android/src/main/res/values/dimens.xml index 81f7d4377..8828a0799 100644 --- a/briar-android/src/main/res/values/dimens.xml +++ b/briar-android/src/main/res/values/dimens.xml @@ -43,7 +43,7 @@ @dimen/message_bubble_radius_small @dimen/message_bubble_radius_big 6dp - 210dp + 115dp 150dp 240dp 100dp