Created a new SectionedRecyclerViewAdapter.

This commit is contained in:
Greyson Parrelli 2019-07-02 17:02:04 -04:00
parent 76d1382d9a
commit 4fbb87b5b7
5 changed files with 214 additions and 111 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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<String, StickerManagementAdapter.StickerSection> {
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<String> stableIdGenerator;
private final GlideRequests glideRequests;
private final EventListener eventListener;
private final List<Section> sections = new ArrayList<Section>(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<StickerSection> sections = new ArrayList<StickerSection>(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<StickerSection> 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<StickerPackRecord> installedPacks,
@NonNull List<StickerPackRecord> availablePacks,
@NonNull List<StickerPackRecord> 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<String> {
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<StickerPackRecord> records;
private final int offset;
Section(@NonNull String tag,
@StringRes int titleResId,
@StringRes int emptyResId,
@NonNull List<StickerPackRecord> records,
int offset)
StickerSection(@NonNull String tag,
@StringRes int titleResId,
@StringRes int emptyResId,
@NonNull List<StickerPackRecord> 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<String> idGenerator, int globalPosition) {
int localPosition = globalPosition - offset;
@Override
public int getContentSize() {
return records.size();
}
@Override
public long getItemId(@NonNull StableIdGenerator<String> 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 {

View File

@ -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 <IdType> The type you'll use to generate stable IDs.
* @param <SectionImpl> The subclass of {@link Section} you're using.
*/
public abstract class SectionedRecyclerViewAdapter<IdType, SectionImpl extends SectionedRecyclerViewAdapter.Section<IdType>> 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<IdType> stableIdGenerator;
public SectionedRecyclerViewAdapter() {
this.stableIdGenerator = new StableIdGenerator<>();
setHasStableIds(true);
}
protected @NonNull abstract List<SectionImpl> 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 <E> The type you'll use to generate stable IDs.
*/
public static abstract class Section<E> {
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<E> 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 {}
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.util;
package org.thoughtcrime.securesms.util.adapter;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;