From c3d44663cd33f29333bbbe166e223e4d29c2873d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 28 Nov 2018 11:38:31 -0200 Subject: [PATCH 1/4] [android] Use a nested RecyclerView with a single items to show image attachments This is preparation for showing multiple image attachments in one message bubble. --- .../conversation/ConversationAdapter.java | 18 ++- .../ConversationItemViewHolder.java | 7 +- .../ConversationMessageViewHolder.java | 123 ++++++------------ .../ConversationNoticeViewHolder.java | 9 +- .../ConversationRequestViewHolder.java | 9 +- .../android/conversation/ImageAdapter.java | 81 ++++++++++++ .../android/conversation/ImageViewHolder.java | 54 ++++++++ .../conversation/SingleImageViewHolder.java | 53 ++++++++ .../list_item_conversation_msg_image.xml | 3 +- .../list_item_conversation_msg_image_text.xml | 3 +- .../layout/list_item_conversation_msg_in.xml | 5 +- .../layout/list_item_conversation_msg_out.xml | 2 +- .../src/main/res/layout/list_item_image.xml | 9 ++ 13 files changed, 270 insertions(+), 106 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/SingleImageViewHolder.java create mode 100644 briar-android/src/main/res/layout/list_item_image.xml 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 e185db386..21d68897b 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 @@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation; import android.content.Context; import android.support.annotation.LayoutRes; import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView.RecycledViewPool; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; @@ -21,11 +22,14 @@ class ConversationAdapter extends BriarAdapter { private ConversationListener listener; + private final RecycledViewPool imageViewPool; ConversationAdapter(Context ctx, ConversationListener conversationListener) { super(ctx, ConversationItem.class); listener = conversationListener; + // This shares the same pool for view recycling between all image lists + imageViewPool = new RecycledViewPool(); } @LayoutRes @@ -42,15 +46,17 @@ class ConversationAdapter type, viewGroup, false); switch (type) { case R.layout.list_item_conversation_msg_in: - return new ConversationMessageViewHolder(v, true); + return new ConversationMessageViewHolder(v, listener, true, + imageViewPool); case R.layout.list_item_conversation_msg_out: - return new ConversationMessageViewHolder(v, false); + return new ConversationMessageViewHolder(v, listener, false, + imageViewPool); case R.layout.list_item_conversation_notice_in: - return new ConversationNoticeViewHolder(v, true); + return new ConversationNoticeViewHolder(v, listener, true); case R.layout.list_item_conversation_notice_out: - return new ConversationNoticeViewHolder(v, false); + return new ConversationNoticeViewHolder(v, listener, false); case R.layout.list_item_conversation_request: - return new ConversationRequestViewHolder(v, true); + return new ConversationRequestViewHolder(v, listener, true); default: throw new IllegalArgumentException("Unknown ConversationItem"); } @@ -59,7 +65,7 @@ class ConversationAdapter @Override public void onBindViewHolder(ConversationItemViewHolder ui, int position) { ConversationItem item = items.get(position); - ui.bind(item, listener); + ui.bind(item); listener.onItemVisible(item); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java index 7d0e25d94..8ffa2ab4b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java @@ -18,14 +18,17 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate; @NotNullByDefault abstract class ConversationItemViewHolder extends ViewHolder { + protected final ConversationListener listener; protected final ConstraintLayout layout; @Nullable private final OutItemViewHolder outViewHolder; private final TextView text; protected final TextView time; - ConversationItemViewHolder(View v, boolean isIncoming) { + ConversationItemViewHolder(View v, ConversationListener listener, + boolean isIncoming) { super(v); + this.listener = listener; this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v); layout = v.findViewById(R.id.layout); text = v.findViewById(R.id.text); @@ -33,7 +36,7 @@ abstract class ConversationItemViewHolder extends ViewHolder { } @CallSuper - void bind(ConversationItem item, ConversationListener listener) { + void bind(ConversationItem item) { if (item.getText() != null) { text.setText(trim(item.getText())); } 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 abef0bffd..13d7c533c 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 @@ -1,64 +1,47 @@ package org.briarproject.briar.android.conversation; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.support.annotation.DrawableRes; 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; import android.view.ViewGroup; -import android.widget.ImageView; - -import com.bumptech.glide.load.Transformation; 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 static android.os.Build.VERSION.SDK_INT; -import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL; -import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; -import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; +import static android.support.constraint.ConstraintSet.WRAP_CONTENT; @UiThread @NotNullByDefault class ConversationMessageViewHolder extends ConversationItemViewHolder { - @DrawableRes - private static final int ERROR_RES = R.drawable.ic_image_broken; - - private final ImageView imageView; + private final ImageAdapter adapter; private final ViewGroup statusLayout; private final int timeColor, timeColorBubble; - private final int radiusBig, radiusSmall; - private final boolean isRtl; private final ConstraintSet textConstraints = new ConstraintSet(); private final ConstraintSet imageConstraints = new ConstraintSet(); private final ConstraintSet imageTextConstraints = new ConstraintSet(); - ConversationMessageViewHolder(View v, boolean isIncoming) { - super(v, isIncoming); - imageView = v.findViewById(R.id.imageView); + ConversationMessageViewHolder(View v, ConversationListener listener, + boolean isIncoming, RecycledViewPool imageViewPool) { + super(v, listener, isIncoming); statusLayout = v.findViewById(R.id.statusLayout); - radiusBig = v.getContext().getResources() - .getDimensionPixelSize(R.dimen.message_bubble_radius_big); - radiusSmall = v.getContext().getResources() - .getDimensionPixelSize(R.dimen.message_bubble_radius_small); + + // image list + RecyclerView list = v.findViewById(R.id.imageView); + list.setRecycledViewPool(imageViewPool); + list.setLayoutManager(new GridLayoutManager(v.getContext(), 2)); + adapter = new ImageAdapter(listener); + list.setAdapter(adapter); // remember original status text color timeColor = time.getCurrentTextColor(); timeColorBubble = ContextCompat.getColor(v.getContext(), R.color.briar_white); - // find out if we are showing a RTL language, Use the configuration, - // because getting the layout direction of views is not reliable - Configuration config = - imageView.getContext().getResources().getConfiguration(); - isRtl = SDK_INT >= 17 && - config.getLayoutDirection() == LAYOUT_DIRECTION_RTL; - // clone constraint sets from layout files textConstraints .clone(v.getContext(), R.layout.list_item_conversation_msg_in); @@ -77,32 +60,24 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { } @Override - void bind(ConversationItem conversationItem, - ConversationListener listener) { - super.bind(conversationItem, listener); + void bind(ConversationItem conversationItem) { + super.bind(conversationItem); ConversationMessageItem item = (ConversationMessageItem) conversationItem; if (item.getAttachments().isEmpty()) { bindTextItem(); } else { - bindImageItem(item, listener); + bindImageItem(item); } } private void bindTextItem() { - clearImage(); - statusLayout.setBackgroundResource(0); - // also reset padding (the background drawable defines some) - statusLayout.setPadding(0, 0, 0, 0); - time.setTextColor(timeColor); + resetStatusLayoutForText(); textConstraints.applyTo(layout); + adapter.clear(); } - private void bindImageItem(ConversationMessageItem item, - ConversationListener listener) { - // TODO show more than just the first image - AttachmentItem attachment = item.getAttachments().get(0); - + private void bindImageItem(ConversationMessageItem item) { ConstraintSet constraintSet; if (item.getText() == null) { statusLayout @@ -110,52 +85,30 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { time.setTextColor(timeColorBubble); constraintSet = imageConstraints; } else { - statusLayout.setBackgroundResource(0); - // also reset padding (the background drawable defines some) - statusLayout.setPadding(0, 0, 0, 0); - time.setTextColor(timeColor); + resetStatusLayoutForText(); constraintSet = imageTextConstraints; } - // apply image size constraints, so glides picks them up for scaling - int width = attachment.getThumbnailWidth(); - int height = attachment.getThumbnailHeight(); - constraintSet.constrainWidth(R.id.imageView, width); - constraintSet.constrainHeight(R.id.imageView, height); - constraintSet.applyTo(layout); - - if (attachment.hasError()) { - clearImage(); - imageView.setImageResource(ERROR_RES); + if (item.getAttachments().size() == 1) { + // apply image size constraints for a single image + 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); } else { - loadImage(item, attachment, listener); + constraintSet.constrainWidth(R.id.imageView, WRAP_CONTENT); + constraintSet.constrainHeight(R.id.imageView, WRAP_CONTENT); } + constraintSet.applyTo(layout); + adapter.setConversationItem(item); } - private void clearImage() { - GlideApp.with(imageView) - .clear(imageView); - imageView.setOnClickListener(null); - } - - private void loadImage(ConversationMessageItem item, - AttachmentItem attachment, ConversationListener listener) { - boolean leftCornerSmall = - (isIncoming() && !isRtl) || (!isIncoming() && isRtl); - boolean bottomRound = item.getText() == null; - Transformation transformation = new BriarImageTransformation( - radiusSmall, radiusBig, leftCornerSmall, bottomRound); - - GlideApp.with(imageView) - .load(attachment) - .diskCacheStrategy(NONE) - .error(ERROR_RES) - .transform(transformation) - .transition(withCrossFade()) - .into(imageView) - .waitForLayout(); - imageView.setOnClickListener( - view -> listener.onAttachmentClicked(view, item, attachment)); + private void resetStatusLayoutForText() { + statusLayout.setBackgroundResource(0); + // also reset padding (the background drawable defines some) + statusLayout.setPadding(0, 0, 0, 0); + time.setTextColor(timeColor); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java index 75b58bcca..8f34e168b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java @@ -19,16 +19,17 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder { private final TextView msgText; - ConversationNoticeViewHolder(View v, boolean isIncoming) { - super(v, isIncoming); + ConversationNoticeViewHolder(View v, ConversationListener listener, + boolean isIncoming) { + super(v, listener, isIncoming); msgText = v.findViewById(R.id.msgText); } @Override @CallSuper - void bind(ConversationItem item, ConversationListener listener) { + void bind(ConversationItem item) { ConversationNoticeItem notice = (ConversationNoticeItem) item; - super.bind(notice, listener); + super.bind(notice); String text = notice.getMsgText(); if (isNullOrEmpty(text)) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java index 76637131f..4afaba399 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java @@ -17,16 +17,17 @@ class ConversationRequestViewHolder extends ConversationNoticeViewHolder { private final Button acceptButton; private final Button declineButton; - ConversationRequestViewHolder(View v, boolean isIncoming) { - super(v, isIncoming); + ConversationRequestViewHolder(View v, ConversationListener listener, + boolean isIncoming) { + super(v, listener, isIncoming); acceptButton = v.findViewById(R.id.acceptButton); declineButton = v.findViewById(R.id.declineButton); } @Override - void bind(ConversationItem item, ConversationListener listener) { + void bind(ConversationItem item) { ConversationRequestItem request = (ConversationRequestItem) item; - super.bind(request, listener); + super.bind(request); if (request.wasAnswered() && request.canBeOpened()) { acceptButton.setVisibility(VISIBLE); 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 new file mode 100644 index 000000000..42e4991a7 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java @@ -0,0 +1,81 @@ +package org.briarproject.briar.android.conversation; + +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView.Adapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.R; + +import java.util.ArrayList; +import java.util.List; + +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; + @Nullable + private ConversationMessageItem conversationItem; + + public ImageAdapter(ConversationListener listener) { + super(); + this.listener = listener; + } + + @Override + public int getItemViewType(int position) { + return items.size() == 1 ? TYPE_SINGLE : TYPE_MULTIPLE; + } + + @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); + } + + @Override + public void onBindViewHolder(ImageViewHolder imageViewHolder, + int position) { + requireNonNull(conversationItem); + AttachmentItem item = items.get(position); + 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); + } + } + + @Override + public int getItemCount() { + return items.size(); + } + + void setConversationItem(ConversationMessageItem item) { + this.conversationItem = item; + this.items.clear(); + this.items.addAll(item.getAttachments()); + notifyDataSetChanged(); + } + + void clear() { + items.clear(); + notifyDataSetChanged(); + } + +} 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 new file mode 100644 index 000000000..4c8985453 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java @@ -0,0 +1,54 @@ +package org.briarproject.briar.android.conversation; + +import android.graphics.Bitmap; +import android.support.annotation.DrawableRes; +import android.support.v7.widget.RecyclerView.ViewHolder; +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.GlideApp; + +import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; + +@NotNullByDefault +class ImageViewHolder extends ViewHolder { + + @DrawableRes + private static final int ERROR_RES = R.drawable.ic_image_broken; + + protected final ImageView imageView; + protected Transformation transformation = new CenterCrop(); + + public ImageViewHolder(View v) { + super(v); + imageView = v.findViewById(R.id.imageView); + } + + void bind(AttachmentItem attachment) { + if (attachment.hasError()) { + GlideApp.with(imageView) + .clear(imageView); + imageView.setImageResource(ERROR_RES); + } else { + loadImage(attachment); + } + } + + private void loadImage(AttachmentItem a) { + GlideApp.with(imageView) + .load(a) + .diskCacheStrategy(NONE) + .error(ERROR_RES) + .transform(transformation) + .transition(withCrossFade()) + .into(imageView) + .waitForLayout(); + } + +} 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 new file mode 100644 index 000000000..f0f964e46 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/SingleImageViewHolder.java @@ -0,0 +1,53 @@ +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/res/layout/list_item_conversation_msg_image.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml index c36a3080c..e3e6d7917 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 @@ -15,7 +15,7 @@ android:background="@drawable/msg_in" android:elevation="@dimen/message_bubble_elevation"> - - - + tools:ignore="ContentDescription" + tools:listitem="@layout/list_item_image"/> - + From 961fdc8e7276298a29959df3eaa82c72885e9bb4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 28 Nov 2018 17:01:32 -0200 Subject: [PATCH 2/4] [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 From dfb71a03a525a9257cad0b4dd928cb1e834cb26b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 4 Dec 2018 19:05:16 -0200 Subject: [PATCH 3/4] [android] Only retrieve image sizes for single images in messages We need to do this to know the height of messages when binding the view. The size of single images can be different (e.g. due to orientation). For multiple images, we use a fixed size, so no retrieval is required. --- .../conversation/AttachmentController.java | 28 +++++++++++++++---- .../conversation/ConversationActivity.java | 4 +-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java index 2d85e2668..c708176b6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java @@ -92,19 +92,32 @@ class AttachmentController { List getAttachmentItems( List> attachments) { + boolean needsSize = attachments.size() == 1; List items = new ArrayList<>(attachments.size()); for (Pair a : attachments) { AttachmentItem item = - getAttachmentItem(a.getFirst(), a.getSecond()); + getAttachmentItem(a.getFirst(), a.getSecond(), needsSize); items.add(item); } return items; } - private AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) { + private AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a, + boolean needsSize) { MessageId messageId = h.getMessageId(); - Size size = new Size(); + if (!needsSize) { + String mimeType = h.getContentType(); + String extension = getExtensionFromMimeType(mimeType); + boolean hasError = false; + if (extension == null) { + extension = ""; + hasError = true; + } + return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0, + 0, hasError); + } + Size size = new Size(); InputStream is = a.getStream(); is.mark(Integer.MAX_VALUE); try { @@ -134,8 +147,7 @@ class AttachmentController { getThumbnailSize(size.width, size.height, size.mimeType); } // get file extension - MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - String extension = mimeTypeMap.getExtensionFromMimeType(size.mimeType); + String extension = getExtensionFromMimeType(size.mimeType); if (extension == null) { return new AttachmentItem(messageId, 0, 0, "", "", 0, 0, true); } @@ -144,6 +156,12 @@ class AttachmentController { size.error); } + @Nullable + private String getExtensionFromMimeType(String mimeType) { + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + return mimeTypeMap.getExtensionFromMimeType(mimeType); + } + /** * Gets the size of a JPEG {@link InputStream} if EXIF info is available. */ diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 3d3611f90..ae8b22970 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -397,7 +397,7 @@ public class ConversationActivity extends BriarActivity textCache.put(id, text); } } - if (!h.getAttachmentHeaders().isEmpty()) { + if (h.getAttachmentHeaders().size() == 1) { List items = attachmentController.get(id); if (items == null) { @@ -486,7 +486,7 @@ public class ConversationActivity extends BriarActivity try { List> attachments = attachmentController.getMessageAttachments(headers); - // TODO move getting the items off to the IoExecutor + // TODO move getting the items off to IoExecutor, if size == 1 List items = attachmentController.getAttachmentItems(attachments); displayMessageAttachments(messageId, items); From 2332a58681422745cb370c171ec1988519d7a13f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 12 Dec 2018 16:55:09 -0200 Subject: [PATCH 4/4] [android] address review comments for displaying multiple images --- .../ConversationMessageViewHolder.java | 3 +- .../android/conversation/ImageAdapter.java | 11 ++++---- .../conversation/ImageItemDecoration.java | 28 ++++++------------- .../android/conversation/ImageViewHolder.java | 9 ++---- .../android/conversation/ImageViewModel.java | 4 +-- .../briar/android/util/UiUtils.java | 7 +++++ 6 files changed, 26 insertions(+), 36 deletions(-) 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 7893071aa..94c80639a 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 @@ -33,8 +33,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { // image list RecyclerView list = v.findViewById(R.id.imageList); list.setRecycledViewPool(imageViewPool); - adapter = - new ImageAdapter(v.getContext(), imageItemDecoration, listener); + adapter = new ImageAdapter(v.getContext(), listener); list.setAdapter(adapter); list.addItemDecoration(imageItemDecoration); 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 134f19975..8c9e15d30 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 @@ -19,36 +19,35 @@ import java.util.List; import static android.content.Context.WINDOW_SERVICE; import static java.util.Objects.requireNonNull; +import static org.briarproject.briar.android.util.UiUtils.isRtl; @NotNullByDefault class ImageAdapter extends Adapter { private final List items = new ArrayList<>(); private final ConversationListener listener; - private final int imageSize, borderSize; + private final int imageSize; private final int radiusBig, radiusSmall; private final boolean isRtl; @Nullable private ConversationMessageItem conversationItem; - public ImageAdapter(Context ctx, ImageItemDecoration imageItemDecoration, - ConversationListener listener) { + ImageAdapter(Context ctx, ConversationListener listener) { this.listener = listener; - 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(); + isRtl = isRtl(ctx); } @Override public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) { View v = LayoutInflater.from(viewGroup.getContext()).inflate( R.layout.list_item_image, viewGroup, false); - return new ImageViewHolder(v, imageSize, borderSize); + return new ImageViewHolder(v, imageSize); } @Override 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 index c01f962c8..92df42e40 100644 --- 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 @@ -1,7 +1,6 @@ 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; @@ -11,9 +10,8 @@ import android.view.View; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.util.UiUtils; -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; @@ -22,24 +20,22 @@ import static org.briarproject.briar.android.conversation.ImageAdapter.singleInR @NotNullByDefault class ImageItemDecoration extends ItemDecoration { - private final int realBorderSize, border; + private final int border; private final boolean isRtl; - public ImageItemDecoration(Context ctx) { + 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;; + int 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; + isRtl = UiUtils.isRtl(ctx); } @Override @@ -48,19 +44,11 @@ class ImageItemDecoration extends ItemDecoration { if (state.getItemCount() == 1) return; int pos = parent.getChildAdapterPosition(view); int num = state.getItemCount(); - boolean left = isLeft(pos) ^ isRtl; + boolean start = isLeft(pos) ^ isRtl; outRect.top = isTopRow(pos) ? 0 : border; - outRect.left = left ? 0 : border; - outRect.right = left && !singleInRow(pos, num) ? border : 0; + outRect.left = start ? 0 : border; + outRect.right = start && !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 271fa52ea..301fa3c13 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 @@ -25,13 +25,12 @@ class ImageViewHolder extends ViewHolder { private static final int ERROR_RES = R.drawable.ic_image_broken; protected final ImageView imageView; - private final int imageSize, borderSize; + private final int imageSize; - public ImageViewHolder(View v, int imageSize, int borderSize) { + ImageViewHolder(View v, int imageSize) { super(v); imageView = v.findViewById(R.id.imageView); this.imageSize = imageSize; - this.borderSize = borderSize; } void bind(AttachmentItem attachment, Radii r, boolean single, @@ -49,9 +48,7 @@ class ImageViewHolder extends ViewHolder { 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; + int width = needsStretch ? imageSize * 2 : imageSize; params.width = single ? a.getThumbnailWidth() : width; params.height = single ? a.getThumbnailHeight() : imageSize; params.setFullSpan(!single && needsStretch); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index 694887c91..d3cc62019 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -48,10 +48,10 @@ public class ImageViewModel extends AndroidViewModel { @IoExecutor private final Executor ioExecutor; - private MutableLiveData saveState = new MutableLiveData<>(); + private final MutableLiveData saveState = new MutableLiveData<>(); @Inject - public ImageViewModel(Application application, + ImageViewModel(Application application, MessagingManager messagingManager, @DatabaseExecutor Executor dbExecutor, @IoExecutor Executor ioExecutor) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index fcd7fcb14..c3aa1038c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -56,6 +56,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.Build.MANUFACTURER; import static android.os.Build.VERSION.SDK_INT; import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; +import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_AUTO; import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_NO; @@ -354,4 +355,10 @@ public class UiUtils { }); } + public static boolean isRtl(Context ctx) { + if (SDK_INT < 17) return false; + return ctx.getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL; + } + }