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"/> - +