From 704b8c48d1fdfeee3d900287347d24e79a72aef8 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Tue, 3 Nov 2020 17:19:38 +0100 Subject: Performant image picker Signed-off-by: Stefan Niedermann --- .../card/attachments/CardAttachmentsFragment.java | 45 +++++---- .../ui/card/attachments/picker/GalleryAdapter.java | 107 ++++++++++++++++++--- .../attachments/picker/GalleryItemViewHolder.java | 19 ++-- .../layout/fragment_card_edit_tab_attachments.xml | 30 ++---- 4 files changed, 138 insertions(+), 63 deletions(-) (limited to 'app') diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java index 990328f66..9edcf7a33 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java @@ -4,12 +4,10 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; -import android.database.Cursor; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; -import android.provider.MediaStore; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; @@ -34,8 +32,6 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import java.io.File; import java.io.IOException; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -156,12 +152,13 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme public void onStateChanged(@NonNull View bottomSheet, int newState) { switch (newState) { case STATE_HIDDEN: { - binding.bottomNavigation.setVisibility(GONE); + hidePicker(); + binding.fab.show(); break; } case STATE_EXPANDED: case STATE_HALF_EXPANDED: { - binding.bottomNavigation.setVisibility(VISIBLE); + showPicker(); break; } } @@ -172,20 +169,11 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } }); - GalleryAdapter galleryAdapter = new GalleryAdapter(); - List imageIds = Collections.emptyList(); if (SDK_INT >= LOLLIPOP) { - final ContentResolver contentResolver = requireContext().getContentResolver(); - try (final Cursor outerCursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { - imageIds = new ArrayList<>(outerCursor.getCount()); - while (outerCursor.moveToNext()) { - imageIds.add(outerCursor.getLong(outerCursor.getColumnIndex(MediaStore.Images.Media._ID))); - } - } + GalleryAdapter galleryAdapter = new GalleryAdapter(requireContext()); + binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3)); + binding.pickerRecyclerView.setAdapter(galleryAdapter); } - binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3)); - galleryAdapter.setImageIds(imageIds); - binding.pickerRecyclerView.setAdapter(galleryAdapter); final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column)); @@ -224,7 +212,8 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme // picker = CardAttachmentPicker.newInstance(); // picker.show(getChildFragmentManager(), CardAttachmentPicker.class.getSimpleName()); mBottomSheetBehaviour.setState(STATE_HALF_EXPANDED); - binding.bottomNavigation.setVisibility(VISIBLE); + showPicker(); + binding.fab.hide(); }); binding.fab.show(); binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -419,6 +408,24 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } } + private void hidePicker() { + binding.bottomNavigation + .animate() + .translationY(binding.bottomNavigation.getHeight()) + .setDuration(300) + .start(); + binding.bottomNavigation.setVisibility(GONE); + } + + private void showPicker() { + binding.bottomNavigation.setVisibility(VISIBLE); + binding.bottomNavigation + .animate() + .translationY(0) + .setDuration(300) + .start(); + } + @Override public void onAttachmentDeleted(Attachment attachment) { adapter.removeAttachment(attachment); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java index faad73e8a..d123cee74 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java @@ -1,35 +1,75 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.picker; +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.util.Pair; +import android.util.Size; import android.view.LayoutInflater; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.Q; +import static androidx.recyclerview.widget.RecyclerView.NO_ID; + public class GalleryAdapter extends RecyclerView.Adapter { @NonNull - List imageIds = new ArrayList<>(); + private final Context context; + private final int columnIndex; + private final int count; + @Nullable + private final Cursor cursor; + @NonNull + private final ContentResolver contentResolver; + @NonNull + private final Map>> itemCache = new HashMap<>(); + private final ExecutorService bitmapFetcherExecutor = Executors.newCachedThreadPool(); + private final ExecutorService bitmapWaiterExecutor = Executors.newCachedThreadPool(); - public GalleryAdapter() { - setHasStableIds(true); - } + public GalleryAdapter(@NonNull Context context) { + this.context = context; + this.contentResolver = context.getContentResolver(); + log("Start Query"); + @SuppressLint("InlinedApi") final String sortOrder = (SDK_INT >= Q) + ? MediaStore.Images.Media.DATE_TAKEN + : MediaStore.Images.Media.DATE_ADDED; + cursor = Objects.requireNonNull(contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID}, null, null, sortOrder + " DESC")); + this.columnIndex = cursor.getColumnIndex(MediaStore.Images.Media._ID); + this.count = cursor.getCount(); - public void setImageIds(@NonNull Collection imageIds) { - this.imageIds.clear(); - this.imageIds.addAll(imageIds); - notifyDataSetChanged(); + log("Cursor count = " + this.count); + notifyItemRangeInserted(0, this.count); + setHasStableIds(true); } @Override public long getItemId(int position) { - return super.getItemId(position); + Pair itemAtPosition = getImageId(position); + return itemAtPosition == null ? NO_ID : itemAtPosition.first; } @NonNull @@ -40,11 +80,50 @@ public class GalleryAdapter extends RecyclerView.Adapter @Override public void onBindViewHolder(@NonNull GalleryItemViewHolder holder, int position) { - holder.bind(imageIds.get(position)); + FutureTask imageFuture = getImageId(position).second; + bitmapFetcherExecutor.execute(imageFuture); + bitmapWaiterExecutor.execute(() -> { + try { + final Bitmap image = imageFuture.get(); + new Handler(Looper.getMainLooper()).post(() -> holder.bind(image)); + } catch (ExecutionException | InterruptedException ignored) { + new Handler(Looper.getMainLooper()).post(() -> holder.bind(null)); + } + }); } @Override public int getItemCount() { - return this.imageIds.size(); + return count; + } + + private Pair> getImageId(int position) { + if (itemCache.containsKey(position)) { + return itemCache.get(position); + } else { + if (cursor == null) { + return new Pair<>(NO_ID, null); + } + if (cursor.moveToPosition(position)) { + long id = cursor.getLong(columnIndex); + return itemCache.put(position, new Pair<>(id, new FutureTask<>(() -> { + if (SDK_INT >= Q) { + return contentResolver.loadThumbnail(ContentUris.withAppendedId( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), new Size(512, 384), null); + } else { + return MediaStore.Images.Thumbnails.getThumbnail( + contentResolver, id, + MediaStore.Images.Thumbnails.MINI_KIND, null); + } + }))); + } else { + throw new NoSuchElementException("Could not find ID for position " + position); + } + } + } + + private void log(String msg) { + DeckLog.log(msg); + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java index 7a409c748..7c4cf4a5a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java @@ -1,30 +1,31 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.picker; -import android.content.Context; -import android.provider.MediaStore; +import android.graphics.Bitmap; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; public class GalleryItemViewHolder extends RecyclerView.ViewHolder { - ItemAttachmentImageBinding binding; + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final ItemAttachmentImageBinding binding; public GalleryItemViewHolder(@NonNull ItemAttachmentImageBinding binding) { super(binding.getRoot()); this.binding = binding; } - public void bind(@NonNull Long imageId) { - Context context = itemView.getContext(); - Glide.with(context) - .load(MediaStore.Images.Thumbnails.getThumbnail( - context.getContentResolver(), imageId, - MediaStore.Images.Thumbnails.MINI_KIND, null)) + public void bind(@Nullable Bitmap image) { + Glide.with(itemView.getContext()) + .load(image) .into(binding.preview); } } diff --git a/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml b/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml index c08656c60..cc02d4b3b 100644 --- a/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml +++ b/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml @@ -37,48 +37,36 @@ tools:visibility="visible" /> + android:layout_marginBottom="10dp" + android:background="@color/bg_info_box" /> - - - - - - - - + android:layout_height="wrap_content" + tools:listitem="@layout/support_simple_spinner_dropdown_item" /> + tools:visibility="visible">