mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-15 12:19:54 +01:00
Merge branch '1438-send-image-attachments-multiple' into 'master'
UX for sending multiple image attachments See merge request briar/briar!1015
This commit is contained in:
@@ -10,12 +10,12 @@ 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 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;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isRtl;
|
||||
|
||||
@NotNullByDefault
|
||||
class ImageItemDecoration extends ItemDecoration {
|
||||
@@ -35,7 +35,7 @@ class ImageItemDecoration extends ItemDecoration {
|
||||
border = realBorderSize / 2;
|
||||
|
||||
// find out if we are showing a RTL language
|
||||
isRtl = UiUtils.isRtl(ctx);
|
||||
isRtl = isRtl(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,43 +1,27 @@
|
||||
package org.briarproject.briar.android.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.constraint.ConstraintLayout;
|
||||
import android.support.v7.graphics.Palette;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
|
||||
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
|
||||
import static android.graphics.Color.BLACK;
|
||||
import static android.graphics.Color.WHITE;
|
||||
import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_YES;
|
||||
import static android.support.v7.app.AppCompatDelegate.getDefaultNightMode;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||
import static com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.FIT_CENTER;
|
||||
import static android.support.v4.content.ContextCompat.getColor;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ImagePreview extends ConstraintLayout {
|
||||
|
||||
private final ImageView imageView;
|
||||
private final int backgroundColor =
|
||||
getDefaultNightMode() == MODE_NIGHT_YES ? BLACK : WHITE;
|
||||
private final RecyclerView imageList;
|
||||
|
||||
@Nullable
|
||||
private ImagePreviewListener listener;
|
||||
@@ -59,9 +43,12 @@ public class ImagePreview extends ConstraintLayout {
|
||||
context.getSystemService(LAYOUT_INFLATER_SERVICE));
|
||||
inflater.inflate(R.layout.image_preview, this, true);
|
||||
|
||||
// find image view and set background color
|
||||
imageView = findViewById(R.id.imageView);
|
||||
imageView.setBackgroundColor(backgroundColor);
|
||||
// set background color
|
||||
setBackgroundColor(getColor(context, R.color.card_background));
|
||||
|
||||
// find list
|
||||
imageList = findViewById(R.id.imageList);
|
||||
imageList.addItemDecoration(new ImagePreviewDecoration(context));
|
||||
|
||||
// set cancel listener
|
||||
findViewById(R.id.imageCancelButton).setOnClickListener(view -> {
|
||||
@@ -73,46 +60,27 @@ public class ImagePreview extends ConstraintLayout {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
void showPreview(List<Uri> imageUris) {
|
||||
void showPreview(Collection<Uri> imageUris) {
|
||||
if (listener == null) throw new IllegalStateException();
|
||||
if (imageUris.size() == 1) {
|
||||
LayoutParams params = (LayoutParams) imageList.getLayoutParams();
|
||||
params.width = MATCH_PARENT;
|
||||
imageList.setLayoutParams(params);
|
||||
}
|
||||
setVisibility(VISIBLE);
|
||||
GlideApp.with(imageView)
|
||||
.asBitmap()
|
||||
.load(imageUris.get(0)) // TODO show more than the first
|
||||
.diskCacheStrategy(NONE)
|
||||
.downsample(FIT_CENTER)
|
||||
.addListener(new RequestListener<Bitmap>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e,
|
||||
Object model, Target<Bitmap> target,
|
||||
boolean isFirstResource) {
|
||||
if (listener != null) listener.onCancel();
|
||||
Toast.makeText(imageView.getContext(),
|
||||
R.string.image_attach_error, LENGTH_LONG)
|
||||
.show();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Bitmap resource,
|
||||
Object model, Target<Bitmap> target,
|
||||
DataSource dataSource, boolean isFirstResource) {
|
||||
Palette.from(resource).generate(
|
||||
ImagePreview.this::onPaletteGenerated);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
imageList.setAdapter(new ImagePreviewAdapter(imageUris, listener));
|
||||
}
|
||||
|
||||
void onPaletteGenerated(@Nullable Palette palette) {
|
||||
if (palette == null) return;
|
||||
int color = getDefaultNightMode() == MODE_NIGHT_YES ?
|
||||
palette.getDarkMutedColor(backgroundColor) :
|
||||
palette.getLightMutedColor(backgroundColor);
|
||||
imageView.setBackgroundColor(color);
|
||||
void removeUri(Uri uri) {
|
||||
ImagePreviewAdapter adapter =
|
||||
(ImagePreviewAdapter) imageList.getAdapter();
|
||||
requireNonNull(adapter).removeUri(uri);
|
||||
}
|
||||
|
||||
interface ImagePreviewListener {
|
||||
|
||||
void onUriError(Uri uri);
|
||||
|
||||
void onCancel();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.briarproject.briar.android.view;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.LayoutRes;
|
||||
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 org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
@NotNullByDefault
|
||||
class ImagePreviewAdapter extends Adapter<ImagePreviewViewHolder> {
|
||||
|
||||
private final List<Uri> items;
|
||||
private final ImagePreviewListener listener;
|
||||
@LayoutRes
|
||||
private final int layout;
|
||||
|
||||
ImagePreviewAdapter(Collection<Uri> items, ImagePreviewListener listener) {
|
||||
this.items = new ArrayList<>(items);
|
||||
this.listener = listener;
|
||||
this.layout = items.size() == 1 ?
|
||||
R.layout.list_item_image_preview_single :
|
||||
R.layout.list_item_image_preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImagePreviewViewHolder onCreateViewHolder(ViewGroup viewGroup,
|
||||
int type) {
|
||||
View v = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(layout, viewGroup, false);
|
||||
return new ImagePreviewViewHolder(v, requireNonNull(listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ImagePreviewViewHolder viewHolder,
|
||||
int position) {
|
||||
viewHolder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
void removeUri(Uri uri) {
|
||||
int pos = items.indexOf(uri);
|
||||
items.remove(uri);
|
||||
notifyItemRemoved(pos);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.briarproject.briar.android.view;
|
||||
|
||||
import android.content.Context;
|
||||
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;
|
||||
|
||||
@NotNullByDefault
|
||||
class ImagePreviewDecoration extends ItemDecoration {
|
||||
|
||||
private final int border;
|
||||
|
||||
ImagePreviewDecoration(Context ctx) {
|
||||
Resources res = ctx.getResources();
|
||||
border = res.getDimensionPixelSize(R.dimen.message_bubble_border);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
|
||||
State state) {
|
||||
if (state.getItemCount() == parent.getChildAdapterPosition(view) + 1) {
|
||||
// no decoration for last item in the list
|
||||
return;
|
||||
}
|
||||
outRect.right = border;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.briarproject.briar.android.view;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
|
||||
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||
import static com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.FIT_CENTER;
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
|
||||
@NotNullByDefault
|
||||
class ImagePreviewViewHolder extends ViewHolder {
|
||||
|
||||
@DrawableRes
|
||||
private static final int ERROR_RES = R.drawable.ic_image_broken;
|
||||
|
||||
private final ImagePreviewListener listener;
|
||||
|
||||
private final ImageView imageView;
|
||||
private final ProgressBar progressBar;
|
||||
|
||||
ImagePreviewViewHolder(View v, ImagePreviewListener listener) {
|
||||
super(v);
|
||||
this.listener = listener;
|
||||
this.imageView = v.findViewById(R.id.imageView);
|
||||
this.progressBar = v.findViewById(R.id.progressBar);
|
||||
}
|
||||
|
||||
void bind(Uri uri) {
|
||||
GlideApp.with(imageView)
|
||||
.load(uri)
|
||||
.diskCacheStrategy(NONE)
|
||||
.error(ERROR_RES)
|
||||
.downsample(FIT_CENTER)
|
||||
.transition(withCrossFade())
|
||||
.addListener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e,
|
||||
Object model, Target<Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
listener.onUriError(uri);
|
||||
progressBar.setVisibility(INVISIBLE);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource,
|
||||
Object model, Target<Drawable> target,
|
||||
DataSource dataSource, boolean isFirstResource) {
|
||||
progressBar.setVisibility(INVISIBLE);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.view.AbsSavedState;
|
||||
import android.support.v7.widget.AppCompatImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
|
||||
|
||||
@@ -26,11 +27,12 @@ import static android.support.v4.view.AbsSavedState.EMPTY_STATE;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
public class TextAttachmentController extends TextSendController
|
||||
implements ImagePreviewListener {
|
||||
|
||||
@@ -81,15 +83,15 @@ public class TextAttachmentController extends TextSendController
|
||||
ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT);
|
||||
intent.addCategory(CATEGORY_OPENABLE);
|
||||
intent.setType("image/*");
|
||||
if (SDK_INT >= 18) // TODO set true to allow attaching multiple images
|
||||
intent.putExtra(EXTRA_ALLOW_MULTIPLE, false);
|
||||
if (SDK_INT >= 18) intent.putExtra(EXTRA_ALLOW_MULTIPLE, true);
|
||||
requireNonNull(imageListener).onAttachImage(intent);
|
||||
}
|
||||
|
||||
public void onImageReceived(@Nullable Intent resultData) {
|
||||
if (resultData == null) return;
|
||||
if (resultData.getData() != null) {
|
||||
imageUris = singletonList(resultData.getData());
|
||||
imageUris = new ArrayList<>(1);
|
||||
imageUris.add(resultData.getData());
|
||||
onNewUris();
|
||||
} else if (SDK_INT >= 18 && resultData.getClipData() != null) {
|
||||
ClipData clipData = resultData.getClipData();
|
||||
@@ -163,13 +165,22 @@ public class TextAttachmentController extends TextSendController
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Parcelable onRestoreInstanceState(@NonNull Parcelable inState) {
|
||||
public Parcelable onRestoreInstanceState(Parcelable inState) {
|
||||
SavedState state = (SavedState) inState;
|
||||
imageUris = state.imageUris;
|
||||
imageUris = requireNonNull(state.imageUris);
|
||||
onNewUris();
|
||||
return state.getSuperState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUriError(Uri uri) {
|
||||
imageUris.remove(uri);
|
||||
imagePreview.removeUri(uri);
|
||||
if (imageUris.isEmpty()) onCancel();
|
||||
Toast.makeText(textInput.getContext(), R.string.image_attach_error,
|
||||
LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
textInput.clearText();
|
||||
@@ -177,6 +188,8 @@ public class TextAttachmentController extends TextSendController
|
||||
}
|
||||
|
||||
private static class SavedState extends AbsSavedState {
|
||||
|
||||
@Nullable
|
||||
private List<Uri> imageUris;
|
||||
|
||||
private SavedState(Parcelable superState) {
|
||||
@@ -195,16 +208,18 @@ public class TextAttachmentController extends TextSendController
|
||||
out.writeList(imageUris);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<SavedState> CREATOR
|
||||
= new Parcelable.Creator<SavedState>() {
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
public static final Creator<SavedState> CREATOR =
|
||||
new Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public interface AttachImageListener {
|
||||
|
||||
@@ -13,21 +13,23 @@
|
||||
android:id="@+id/divider"
|
||||
style="@style/Divider.Horizontal"
|
||||
android:layout_alignParentTop="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/imageView"
|
||||
app:layout_constraintBottom_toTopOf="@+id/imageList"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/imageList"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:srcCompat="@tools:sample/avatars"/>
|
||||
tools:listitem="@layout/list_item_image"/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/imageCancelButton"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
tools:layout_height="200dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:srcCompat="@tools:sample/avatars"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:layout_height="200dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:srcCompat="@tools:sample/backgrounds/scenic"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
Reference in New Issue
Block a user