Commit c3d44663 authored by Torsten Grote's avatar Torsten Grote

[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.
parent cdf4f3a2
......@@ -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<ConversationItem, ConversationItemViewHolder> {
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);
}
......
......@@ -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()));
}
......
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<Bitmap> 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);
}
}
......@@ -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)) {
......
......@@ -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);
......
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<ImageViewHolder> {
private final static int TYPE_SINGLE = 0;
private final static int TYPE_MULTIPLE = 1;
private final List<AttachmentItem> 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();
}
}
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<Bitmap> 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();
}
}
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);
}
}
......@@ -15,7 +15,7 @@
android:background="@drawable/msg_in"
android:elevation="@dimen/message_bubble_elevation">
<ImageView
<android.support.v7.widget.RecyclerView
android:id="@+id/imageView"
android:layout_width="@dimen/message_bubble_image_default"
android:layout_height="@dimen/message_bubble_image_default"
......@@ -23,6 +23,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:listitem="@layout/list_item_image"
tools:src="@drawable/alerts_and_states_error"/>
<com.vanniktech.emoji.EmojiTextView
......
......@@ -15,7 +15,7 @@
android:background="@drawable/msg_in"
android:elevation="@dimen/message_bubble_elevation">
<ImageView
<android.support.v7.widget.RecyclerView
android:id="@+id/imageView"
android:layout_width="@dimen/message_bubble_image_default"
android:layout_height="@dimen/message_bubble_image_default"
......@@ -23,6 +23,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:listitem="@layout/list_item_image"
tools:src="@drawable/alerts_and_states_error"/>
<com.vanniktech.emoji.EmojiTextView
......
......@@ -15,7 +15,7 @@
android:background="@drawable/msg_in"
android:elevation="@dimen/message_bubble_elevation">
<ImageView
<android.support.v7.widget.RecyclerView
android:id="@+id/imageView"
android:layout_width="@dimen/message_bubble_image_default"
android:layout_height="@dimen/message_bubble_image_default"
......@@ -23,7 +23,8 @@
app:layout_constraintBottom_toTopOf="@+id/text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"/>
tools:ignore="ContentDescription"
tools:listitem="@layout/list_item_image"/>
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/text"
......
......@@ -21,7 +21,7 @@
android:background="@drawable/msg_out"
android:elevation="@dimen/message_bubble_elevation">
<ImageView
<android.support.v7.widget.RecyclerView
android:id="@+id/imageView"
android:layout_width="@dimen/message_bubble_image_default"
android:layout_height="@dimen/message_bubble_image_default"
......
<?xml version="1.0" encoding="utf-8"?>
<ImageView
android:id="@+id/imageView"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="@dimen/message_bubble_image_default"
android:layout_height="@dimen/message_bubble_image_default"
tools:ignore="ContentDescription"
tools:src="@drawable/alerts_and_states_error"/>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment