diff --git a/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java b/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java index 22760834f7..e318526d18 100644 --- a/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java +++ b/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java @@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.util.StableIdGenerator; +import org.thoughtcrime.securesms.util.adapter.StableIdGenerator; import java.util.ArrayList; import java.util.List; diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java index 293b394e74..1d92fba4f9 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java @@ -14,7 +14,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.StableIdGenerator; +import org.thoughtcrime.securesms.util.adapter.StableIdGenerator; import java.util.ArrayList; import java.util.Collection; diff --git a/src/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java b/src/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java index 24fd5b0811..5dd217003e 100644 --- a/src/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java +++ b/src/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.stickers; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.recyclerview.widget.RecyclerView; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -16,92 +18,65 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.model.StickerPackRecord; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.util.StableIdGenerator; +import org.thoughtcrime.securesms.util.adapter.SectionedRecyclerViewAdapter; +import org.thoughtcrime.securesms.util.adapter.StableIdGenerator; import java.util.ArrayList; import java.util.List; -final class StickerManagementAdapter extends RecyclerView.Adapter { - - private static final int TYPE_HEADER = 1; - private static final int TYPE_EMPTY = 2; - private static final int TYPE_PACK = 3; +final class StickerManagementAdapter extends SectionedRecyclerViewAdapter { private static final String TAG_YOUR_STICKERS = "YourStickers"; private static final String TAG_MESSAGE_STICKERS = "MessageStickers"; private static final String TAG_BLESSED_STICKERS = "BlessedStickers"; - private final GlideRequests glideRequests; - private final EventListener eventListener; - private final StableIdGenerator stableIdGenerator; + private final GlideRequests glideRequests; + private final EventListener eventListener; - private final List
sections = new ArrayList
(3) {{ - Section yourStickers = new Section(TAG_YOUR_STICKERS, - R.string.StickerManagementAdapter_installed_stickers, - R.string.StickerManagementAdapter_no_stickers_installed, - new ArrayList<>(), - 0); - Section messageStickers = new Section(TAG_MESSAGE_STICKERS, - R.string.StickerManagementAdapter_stickers_you_received, - R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here, - new ArrayList<>(), - yourStickers.size()); + private final List sections = new ArrayList(3) {{ + StickerSection yourStickers = new StickerSection(TAG_YOUR_STICKERS, + R.string.StickerManagementAdapter_installed_stickers, + R.string.StickerManagementAdapter_no_stickers_installed, + new ArrayList<>(), + 0); + StickerSection messageStickers = new StickerSection(TAG_MESSAGE_STICKERS, + R.string.StickerManagementAdapter_stickers_you_received, + R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here, + new ArrayList<>(), + yourStickers.size()); add(yourStickers); add(messageStickers); }}; StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) { - this.glideRequests = glideRequests; - this.eventListener = eventListener; - this.stableIdGenerator = new StableIdGenerator<>(); - - setHasStableIds(true); + this.glideRequests = glideRequests; + this.eventListener = eventListener; } @Override - public long getItemId(int position) { - for (Section section : sections) { - if (section.handles(position)) { - return section.getItemId(stableIdGenerator, position); - } - } - throw new NoSectionException(); + protected @NonNull List getSections() { + return sections; } @Override - public int getItemViewType(int position) { - for (Section section : sections) { - if (section.handles(position)) { - return section.getViewType(position); - } - } - throw new NoSectionException(); + protected @NonNull RecyclerView.ViewHolder createHeaderViewHolder(@NonNull ViewGroup parent) { + return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.sticker_management_header_item, parent, false)); } @Override - public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { - switch (viewType) { - case TYPE_HEADER: - return new HeaderViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_header_item, viewGroup, false)); - case TYPE_EMPTY: - return new EmptyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_empty_item, viewGroup, false)); - case TYPE_PACK: - return new StickerViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_sticker_item, viewGroup, false)); - default: - throw new AssertionError("Unexpected viewType! " + viewType); - } + protected @NonNull RecyclerView.ViewHolder createContentViewHolder(@NonNull ViewGroup parent) { + return new StickerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.sticker_management_sticker_item, parent, false)); } @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - for (Section section : sections) { - if (section.handles(position)) { - section.bindViewHolder(viewHolder, position, glideRequests, eventListener); - return; - } - } - throw new NoSectionException(); + protected @NonNull RecyclerView.ViewHolder createEmptyViewHolder(@NonNull ViewGroup viewGroup) { + return new EmptyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_empty_item, viewGroup, false)); + } + + @Override + public void bindViewHolder(@NonNull StickerSection section, @NonNull RecyclerView.ViewHolder viewHolder, int position) { + section.bindViewHolder(viewHolder, position, glideRequests, eventListener); } @Override @@ -111,30 +86,25 @@ final class StickerManagementAdapter extends RecyclerView.Adapter { } } - @Override - public int getItemCount() { - return Stream.of(sections).reduce(0, (sum, section) -> sum + section.size()); - } - void setPackLists(@NonNull List installedPacks, @NonNull List availablePacks, @NonNull List blessedPacks) { - Section yourStickers = new Section(TAG_YOUR_STICKERS, - R.string.StickerManagementAdapter_installed_stickers, - R.string.StickerManagementAdapter_no_stickers_installed, - installedPacks, - 0); - Section blessedStickers = new Section(TAG_BLESSED_STICKERS, - R.string.StickerManagementAdapter_signal_artist_series, - 0, - blessedPacks, - yourStickers.size()); - Section messageStickers = new Section(TAG_MESSAGE_STICKERS, - R.string.StickerManagementAdapter_stickers_you_received, - R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here, - availablePacks, - yourStickers.size() + (blessedPacks.isEmpty() ? 0 : blessedStickers.size())); + StickerSection yourStickers = new StickerSection(TAG_YOUR_STICKERS, + R.string.StickerManagementAdapter_installed_stickers, + R.string.StickerManagementAdapter_no_stickers_installed, + installedPacks, + 0); + StickerSection blessedStickers = new StickerSection(TAG_BLESSED_STICKERS, + R.string.StickerManagementAdapter_signal_artist_series, + 0, + blessedPacks, + yourStickers.size()); + StickerSection messageStickers = new StickerSection(TAG_MESSAGE_STICKERS, + R.string.StickerManagementAdapter_stickers_you_received, + R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here, + availablePacks, + yourStickers.size() + (blessedPacks.isEmpty() ? 0 : blessedStickers.size())); sections.clear(); sections.add(yourStickers); @@ -148,7 +118,8 @@ final class StickerManagementAdapter extends RecyclerView.Adapter { notifyDataSetChanged(); } - private static class Section { + public static class StickerSection extends SectionedRecyclerViewAdapter.Section { + private static final String STABLE_ID_HEADER = "header"; private static final String STABLE_ID_TEXT = "text"; @@ -156,35 +127,34 @@ final class StickerManagementAdapter extends RecyclerView.Adapter { private final int titleResId; private final int emptyResId; private final List records; - private final int offset; - Section(@NonNull String tag, - @StringRes int titleResId, - @StringRes int emptyResId, - @NonNull List records, - int offset) + StickerSection(@NonNull String tag, + @StringRes int titleResId, + @StringRes int emptyResId, + @NonNull List records, + int offset) { + super(offset); + this.tag = tag; this.titleResId = titleResId; this.emptyResId = emptyResId; this.records = records; - this.offset = offset; } - int getViewType(int globalPosition) { - int localPosition = globalPosition - offset; - - if (localPosition == 0) { - return TYPE_HEADER; - } else if (records.isEmpty()) { - return TYPE_EMPTY; - } else { - return TYPE_PACK; - } + @Override + public boolean hasEmptyState() { + return true; } - long getItemId(@NonNull StableIdGenerator idGenerator, int globalPosition) { - int localPosition = globalPosition - offset; + @Override + public int getContentSize() { + return records.size(); + } + + @Override + public long getItemId(@NonNull StableIdGenerator idGenerator, int globalPosition) { + int localPosition = getLocalPosition(globalPosition); if (localPosition == 0) { return idGenerator.getId(tag + "_" + STABLE_ID_HEADER); @@ -200,7 +170,7 @@ final class StickerManagementAdapter extends RecyclerView.Adapter { @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) { - int localPosition = globalPosition - offset; + int localPosition = getLocalPosition(globalPosition); if (localPosition == 0) { ((HeaderViewHolder) viewHolder).bind(titleResId); @@ -210,15 +180,6 @@ final class StickerManagementAdapter extends RecyclerView.Adapter { ((StickerViewHolder) viewHolder).bind(glideRequests, eventListener, records.get(localPosition - 1), localPosition == records.size()); } } - - boolean handles(int globalPosition) { - int localPosition = globalPosition - offset; - return localPosition >= 0 && localPosition < size(); - } - - int size() { - return records.isEmpty() ? 2 : records.size() + 1; - } } static class StickerViewHolder extends RecyclerView.ViewHolder { diff --git a/src/org/thoughtcrime/securesms/util/adapter/SectionedRecyclerViewAdapter.java b/src/org/thoughtcrime/securesms/util/adapter/SectionedRecyclerViewAdapter.java new file mode 100644 index 0000000000..977e128ff5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/adapter/SectionedRecyclerViewAdapter.java @@ -0,0 +1,142 @@ +package org.thoughtcrime.securesms.util.adapter; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.annimon.stream.Stream; + +import java.util.List; + +/** + * A {@link RecyclerView.Adapter} subclass that makes it easier to have sectioned content, where + * you have header rows and content rows. + * + * @param The type you'll use to generate stable IDs. + * @param The subclass of {@link Section} you're using. + */ +public abstract class SectionedRecyclerViewAdapter> extends RecyclerView.Adapter { + + private static final int TYPE_HEADER = 1; + private static final int TYPE_CONTENT = 2; + private static final int TYPE_EMPTY = 3; + + private final StableIdGenerator stableIdGenerator; + + public SectionedRecyclerViewAdapter() { + this.stableIdGenerator = new StableIdGenerator<>(); + setHasStableIds(true); + } + + protected @NonNull abstract List getSections(); + protected @NonNull abstract RecyclerView.ViewHolder createHeaderViewHolder(@NonNull ViewGroup parent); + protected @NonNull abstract RecyclerView.ViewHolder createContentViewHolder(@NonNull ViewGroup parent); + protected @Nullable abstract RecyclerView.ViewHolder createEmptyViewHolder(@NonNull ViewGroup viewGroup); + protected abstract void bindViewHolder(@NonNull SectionImpl section, @NonNull RecyclerView.ViewHolder holder, int position); + + @Override + public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + switch (viewType) { + case TYPE_HEADER: + return createHeaderViewHolder(viewGroup); + case TYPE_CONTENT: + return createContentViewHolder(viewGroup); + case TYPE_EMPTY: + RecyclerView.ViewHolder holder = createEmptyViewHolder(viewGroup); + if (holder == null) { + throw new IllegalStateException("Expected an empty view holder, but got none!"); + } + return holder; + default: + throw new AssertionError("Unexpected viewType! " + viewType); + } + } + + @Override + public long getItemId(int position) { + for (SectionImpl section: getSections()) { + if (section.handles(position)) { + return section.getItemId(stableIdGenerator, position); + } + } + throw new NoSectionException(); + } + + @Override + public int getItemViewType(int position) { + for (SectionImpl section : getSections()) { + if (section.handles(position)) { + return section.getViewType(position); + } + } + throw new NoSectionException(); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + for (SectionImpl section : getSections()) { + if (section.handles(position)) { + bindViewHolder(section, holder, position); + return; + } + } + throw new NoSectionException(); + } + + @Override + public int getItemCount() { + return Stream.of(getSections()).reduce(0, (sum, section) -> sum + section.size()); + } + + /** + * Represents a section of content in the adapter. Has a header and content. + * @param The type you'll use to generate stable IDs. + */ + public static abstract class Section { + + private final int offset; + + public Section(int offset) { + this.offset = offset; + } + + public abstract boolean hasEmptyState(); + public abstract int getContentSize(); + public abstract long getItemId(@NonNull StableIdGenerator idGenerator, int globalPosition); + + protected int getLocalPosition(int globalPosition) { + return globalPosition - offset; + } + + public int getViewType(int globalPosition) { + int localPosition = globalPosition - offset; + + if (localPosition == 0) { + return TYPE_HEADER; + } else if (getContentSize() == 0) { + return TYPE_EMPTY; + } else { + return TYPE_CONTENT; + } + } + + public boolean handles(int globalPosition) { + int localPosition = globalPosition - offset; + return localPosition >= 0 && localPosition < size(); + } + + public int size() { + if (getContentSize() == 0 && hasEmptyState()) { + return 2; + } else if (getContentSize() == 0) { + return 0; + } else { + return getContentSize() + 1; + } + } + } + + private static class NoSectionException extends IllegalStateException {} +} diff --git a/src/org/thoughtcrime/securesms/util/StableIdGenerator.java b/src/org/thoughtcrime/securesms/util/adapter/StableIdGenerator.java similarity index 93% rename from src/org/thoughtcrime/securesms/util/StableIdGenerator.java rename to src/org/thoughtcrime/securesms/util/adapter/StableIdGenerator.java index e380f21f95..c8df1e0852 100644 --- a/src/org/thoughtcrime/securesms/util/StableIdGenerator.java +++ b/src/org/thoughtcrime/securesms/util/adapter/StableIdGenerator.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.util; +package org.thoughtcrime.securesms.util.adapter; import androidx.annotation.MainThread; import androidx.annotation.NonNull;