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.components.ThumbnailView;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideRequests; 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.ArrayList;
import java.util.List; 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.R;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.MediaUtil; 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.ArrayList;
import java.util.Collection; import java.util.Collection;

View File

@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.stickers; package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -16,16 +18,13 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.StickerPackRecord; import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests; 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.ArrayList;
import java.util.List; import java.util.List;
final class StickerManagementAdapter extends RecyclerView.Adapter { final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String, StickerManagementAdapter.StickerSection> {
private static final int TYPE_HEADER = 1;
private static final int TYPE_EMPTY = 2;
private static final int TYPE_PACK = 3;
private static final String TAG_YOUR_STICKERS = "YourStickers"; private static final String TAG_YOUR_STICKERS = "YourStickers";
private static final String TAG_MESSAGE_STICKERS = "MessageStickers"; private static final String TAG_MESSAGE_STICKERS = "MessageStickers";
@ -33,15 +32,14 @@ final class StickerManagementAdapter extends RecyclerView.Adapter {
private final GlideRequests glideRequests; private final GlideRequests glideRequests;
private final EventListener eventListener; private final EventListener eventListener;
private final StableIdGenerator<String> stableIdGenerator;
private final List<Section> sections = new ArrayList<Section>(3) {{ private final List<StickerSection> sections = new ArrayList<StickerSection>(3) {{
Section yourStickers = new Section(TAG_YOUR_STICKERS, StickerSection yourStickers = new StickerSection(TAG_YOUR_STICKERS,
R.string.StickerManagementAdapter_installed_stickers, R.string.StickerManagementAdapter_installed_stickers,
R.string.StickerManagementAdapter_no_stickers_installed, R.string.StickerManagementAdapter_no_stickers_installed,
new ArrayList<>(), new ArrayList<>(),
0); 0);
Section messageStickers = new Section(TAG_MESSAGE_STICKERS, StickerSection messageStickers = new StickerSection(TAG_MESSAGE_STICKERS,
R.string.StickerManagementAdapter_stickers_you_received, R.string.StickerManagementAdapter_stickers_you_received,
R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here, R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here,
new ArrayList<>(), new ArrayList<>(),
@ -54,54 +52,31 @@ final class StickerManagementAdapter extends RecyclerView.Adapter {
StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) { StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.eventListener = eventListener; this.eventListener = eventListener;
this.stableIdGenerator = new StableIdGenerator<>();
setHasStableIds(true);
} }
@Override @Override
public long getItemId(int position) { protected @NonNull List<StickerSection> getSections() {
for (Section section : sections) { return sections;
if (section.handles(position)) {
return section.getItemId(stableIdGenerator, position);
}
}
throw new NoSectionException();
} }
@Override @Override
public int getItemViewType(int position) { protected @NonNull RecyclerView.ViewHolder createHeaderViewHolder(@NonNull ViewGroup parent) {
for (Section section : sections) { return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.sticker_management_header_item, parent, false));
if (section.handles(position)) {
return section.getViewType(position);
}
}
throw new NoSectionException();
} }
@Override @Override
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { protected @NonNull RecyclerView.ViewHolder createContentViewHolder(@NonNull ViewGroup parent) {
switch (viewType) { return new StickerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.sticker_management_sticker_item, parent, false));
case TYPE_HEADER: }
return new HeaderViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_header_item, viewGroup, false));
case TYPE_EMPTY: @Override
protected @NonNull RecyclerView.ViewHolder createEmptyViewHolder(@NonNull ViewGroup viewGroup) {
return new EmptyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_empty_item, viewGroup, false)); 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);
}
} }
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void bindViewHolder(@NonNull StickerSection section, @NonNull RecyclerView.ViewHolder viewHolder, int position) {
for (Section section : sections) {
if (section.handles(position)) {
section.bindViewHolder(viewHolder, position, glideRequests, eventListener); section.bindViewHolder(viewHolder, position, glideRequests, eventListener);
return;
}
}
throw new NoSectionException();
} }
@Override @Override
@ -111,26 +86,21 @@ 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, void setPackLists(@NonNull List<StickerPackRecord> installedPacks,
@NonNull List<StickerPackRecord> availablePacks, @NonNull List<StickerPackRecord> availablePacks,
@NonNull List<StickerPackRecord> blessedPacks) @NonNull List<StickerPackRecord> blessedPacks)
{ {
Section yourStickers = new Section(TAG_YOUR_STICKERS, StickerSection yourStickers = new StickerSection(TAG_YOUR_STICKERS,
R.string.StickerManagementAdapter_installed_stickers, R.string.StickerManagementAdapter_installed_stickers,
R.string.StickerManagementAdapter_no_stickers_installed, R.string.StickerManagementAdapter_no_stickers_installed,
installedPacks, installedPacks,
0); 0);
Section blessedStickers = new Section(TAG_BLESSED_STICKERS, StickerSection blessedStickers = new StickerSection(TAG_BLESSED_STICKERS,
R.string.StickerManagementAdapter_signal_artist_series, R.string.StickerManagementAdapter_signal_artist_series,
0, 0,
blessedPacks, blessedPacks,
yourStickers.size()); yourStickers.size());
Section messageStickers = new Section(TAG_MESSAGE_STICKERS, StickerSection messageStickers = new StickerSection(TAG_MESSAGE_STICKERS,
R.string.StickerManagementAdapter_stickers_you_received, R.string.StickerManagementAdapter_stickers_you_received,
R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here, R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here,
availablePacks, availablePacks,
@ -148,7 +118,8 @@ final class StickerManagementAdapter extends RecyclerView.Adapter {
notifyDataSetChanged(); 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_HEADER = "header";
private static final String STABLE_ID_TEXT = "text"; 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 titleResId;
private final int emptyResId; private final int emptyResId;
private final List<StickerPackRecord> records; private final List<StickerPackRecord> records;
private final int offset;
Section(@NonNull String tag, StickerSection(@NonNull String tag,
@StringRes int titleResId, @StringRes int titleResId,
@StringRes int emptyResId, @StringRes int emptyResId,
@NonNull List<StickerPackRecord> records, @NonNull List<StickerPackRecord> records,
int offset) int offset)
{ {
super(offset);
this.tag = tag; this.tag = tag;
this.titleResId = titleResId; this.titleResId = titleResId;
this.emptyResId = emptyResId; this.emptyResId = emptyResId;
this.records = records; this.records = records;
this.offset = offset;
} }
int getViewType(int globalPosition) { @Override
int localPosition = globalPosition - offset; public boolean hasEmptyState() {
return true;
if (localPosition == 0) {
return TYPE_HEADER;
} else if (records.isEmpty()) {
return TYPE_EMPTY;
} else {
return TYPE_PACK;
}
} }
long getItemId(@NonNull StableIdGenerator<String> idGenerator, int globalPosition) { @Override
int localPosition = globalPosition - offset; public int getContentSize() {
return records.size();
}
@Override
public long getItemId(@NonNull StableIdGenerator<String> idGenerator, int globalPosition) {
int localPosition = getLocalPosition(globalPosition);
if (localPosition == 0) { if (localPosition == 0) {
return idGenerator.getId(tag + "_" + STABLE_ID_HEADER); return idGenerator.getId(tag + "_" + STABLE_ID_HEADER);
@ -200,7 +170,7 @@ final class StickerManagementAdapter extends RecyclerView.Adapter {
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener) @NonNull EventListener eventListener)
{ {
int localPosition = globalPosition - offset; int localPosition = getLocalPosition(globalPosition);
if (localPosition == 0) { if (localPosition == 0) {
((HeaderViewHolder) viewHolder).bind(titleResId); ((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()); ((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 { 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.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;