kopia lustrzana https://github.com/ryukoposting/Signal-Android
614 wiersze
22 KiB
Java
614 wiersze
22 KiB
Java
package com.codewaves.stickyheadergrid;
|
|
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
import java.security.InvalidParameterException;
|
|
import java.util.ArrayList;
|
|
|
|
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
|
|
|
|
|
/**
|
|
* Created by Sergej Kravcenko on 4/24/2017.
|
|
* Copyright (c) 2017 Sergej Kravcenko
|
|
*/
|
|
|
|
@SuppressWarnings({"unused", "WeakerAccess"})
|
|
public abstract class StickyHeaderGridAdapter extends RecyclerView.Adapter<StickyHeaderGridAdapter.ViewHolder> {
|
|
public static final String TAG = "StickyHeaderGridAdapter";
|
|
|
|
public static final int TYPE_HEADER = 0;
|
|
public static final int TYPE_ITEM = 1;
|
|
|
|
private ArrayList<Section> mSections;
|
|
private int[] mSectionIndices;
|
|
private int mTotalItemNumber;
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
public static class ViewHolder extends RecyclerView.ViewHolder {
|
|
public ViewHolder(View itemView) {
|
|
super(itemView);
|
|
}
|
|
|
|
public boolean isHeader() {
|
|
return false;
|
|
}
|
|
|
|
|
|
public int getSectionItemViewType() {
|
|
return StickyHeaderGridAdapter.externalViewType(getItemViewType());
|
|
}
|
|
}
|
|
|
|
public static class ItemViewHolder extends ViewHolder {
|
|
public ItemViewHolder(View itemView) {
|
|
super(itemView);
|
|
}
|
|
}
|
|
|
|
public static class HeaderViewHolder extends ViewHolder {
|
|
public HeaderViewHolder(View itemView) {
|
|
super(itemView);
|
|
}
|
|
|
|
@Override
|
|
public boolean isHeader() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static class Section {
|
|
private int position;
|
|
private int itemNumber;
|
|
private int length;
|
|
}
|
|
|
|
private void calculateSections() {
|
|
mSections = new ArrayList<>();
|
|
|
|
int total = 0;
|
|
int sectionCount = getSectionCount();
|
|
for (int s = 0; s < sectionCount; s++) {
|
|
final Section section = new Section();
|
|
section.position = total;
|
|
section.itemNumber = getSectionItemCount(s);
|
|
section.length = section.itemNumber + 1;
|
|
mSections.add(section);
|
|
|
|
total += section.length;
|
|
}
|
|
mTotalItemNumber = total;
|
|
|
|
total = 0;
|
|
mSectionIndices = new int[mTotalItemNumber];
|
|
for (int s = 0; s < sectionCount; s++) {
|
|
final Section section = mSections.get(s);
|
|
for (int i = 0; i < section.length; i++) {
|
|
mSectionIndices[total + i] = s;
|
|
}
|
|
total += section.length;
|
|
}
|
|
}
|
|
|
|
protected int getItemViewInternalType(int position) {
|
|
final int section = getAdapterPositionSection(position);
|
|
final Section sectionObject = mSections.get(section);
|
|
final int sectionPosition = position - sectionObject.position;
|
|
|
|
return getItemViewInternalType(section, sectionPosition);
|
|
}
|
|
|
|
private int getItemViewInternalType(int section, int position) {
|
|
return position == 0 ? TYPE_HEADER : TYPE_ITEM;
|
|
}
|
|
|
|
static private int internalViewType(int type) {
|
|
return type & 0xFF;
|
|
}
|
|
|
|
static private int externalViewType(int type) {
|
|
return type >> 8;
|
|
}
|
|
|
|
@Override
|
|
final public int getItemCount() {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
}
|
|
return mTotalItemNumber;
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
final public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
final int internalType = internalViewType(viewType);
|
|
final int externalType = externalViewType(viewType);
|
|
|
|
switch (internalType) {
|
|
case TYPE_HEADER:
|
|
return onCreateHeaderViewHolder(parent, externalType);
|
|
case TYPE_ITEM:
|
|
return onCreateItemViewHolder(parent, externalType);
|
|
default:
|
|
throw new InvalidParameterException("Invalid viewType: " + viewType);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
final public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
}
|
|
|
|
final int section = mSectionIndices[position];
|
|
final int internalType = internalViewType(holder.getItemViewType());
|
|
final int externalType = externalViewType(holder.getItemViewType());
|
|
|
|
switch (internalType) {
|
|
case TYPE_HEADER:
|
|
onBindHeaderViewHolder((HeaderViewHolder)holder, section);
|
|
break;
|
|
case TYPE_ITEM:
|
|
final ItemViewHolder itemHolder = (ItemViewHolder)holder;
|
|
final int offset = getItemSectionOffset(section, position);
|
|
onBindItemViewHolder((ItemViewHolder)holder, section, offset);
|
|
break;
|
|
default:
|
|
throw new InvalidParameterException("invalid viewType: " + internalType);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
final public int getItemViewType(int position) {
|
|
final int section = getAdapterPositionSection(position);
|
|
final Section sectionObject = mSections.get(section);
|
|
final int sectionPosition = position - sectionObject.position;
|
|
final int internalType = getItemViewInternalType(section, sectionPosition);
|
|
int externalType = 0;
|
|
|
|
switch (internalType) {
|
|
case TYPE_HEADER:
|
|
externalType = getSectionHeaderViewType(section);
|
|
break;
|
|
case TYPE_ITEM:
|
|
externalType = getSectionItemViewType(section, sectionPosition - 1);
|
|
break;
|
|
}
|
|
|
|
return ((externalType & 0xFF) << 8) | (internalType & 0xFF);
|
|
}
|
|
|
|
// Helpers
|
|
private int getItemSectionHeaderPosition(int position) {
|
|
return getSectionHeaderPosition(getAdapterPositionSection(position));
|
|
}
|
|
|
|
private int getAdapterPosition(int section, int offset) {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
}
|
|
|
|
if (section < 0) {
|
|
throw new IndexOutOfBoundsException("section " + section + " < 0");
|
|
}
|
|
|
|
if (section >= mSections.size()) {
|
|
throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size());
|
|
}
|
|
|
|
final Section sectionObject = mSections.get(section);
|
|
return sectionObject.position + offset;
|
|
}
|
|
|
|
/**
|
|
* Given a <code>section</code> and an adapter <code>position</code> get the offset of an item
|
|
* inside <code>section</code>.
|
|
*
|
|
* @param section section to query
|
|
* @param position adapter position
|
|
* @return The item offset inside the section.
|
|
*/
|
|
public int getItemSectionOffset(int section, int position) {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
}
|
|
|
|
if (section < 0) {
|
|
throw new IndexOutOfBoundsException("section " + section + " < 0");
|
|
}
|
|
|
|
if (section >= mSections.size()) {
|
|
throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size());
|
|
}
|
|
|
|
final Section sectionObject = mSections.get(section);
|
|
final int localPosition = position - sectionObject.position;
|
|
if (localPosition >= sectionObject.length) {
|
|
throw new IndexOutOfBoundsException("localPosition: " + localPosition + " >=" + sectionObject.length);
|
|
}
|
|
|
|
return localPosition - 1;
|
|
}
|
|
|
|
/**
|
|
* Returns the section index having item or header with provided
|
|
* provider <code>position</code>.
|
|
*
|
|
* @param position adapter position
|
|
* @return The section containing provided adapter position.
|
|
*/
|
|
public int getAdapterPositionSection(int position) {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
}
|
|
|
|
if (getItemCount() == 0) {
|
|
return NO_POSITION;
|
|
}
|
|
|
|
if (position < 0) {
|
|
throw new IndexOutOfBoundsException("position " + position + " < 0");
|
|
}
|
|
|
|
if (position >= getItemCount()) {
|
|
throw new IndexOutOfBoundsException("position " + position + " >=" + getItemCount());
|
|
}
|
|
|
|
return mSectionIndices[position];
|
|
}
|
|
|
|
/**
|
|
* Returns the adapter position for given <code>section</code> header. Use
|
|
* this only for {@link RecyclerView#scrollToPosition(int)} or similar functions.
|
|
* Never directly manipulate adapter items using this position.
|
|
*
|
|
* @param section section to query
|
|
* @return The adapter position.
|
|
*/
|
|
public int getSectionHeaderPosition(int section) {
|
|
return getAdapterPosition(section, 0);
|
|
}
|
|
|
|
/**
|
|
* Returns the adapter position for given <code>section</code> and
|
|
* <code>offset</code>. Use this only for {@link RecyclerView#scrollToPosition(int)}
|
|
* or similar functions. Never directly manipulate adapter items using this position.
|
|
*
|
|
* @param section section to query
|
|
* @param position item position inside the <code>section</code>
|
|
* @return The adapter position.
|
|
*/
|
|
public int getSectionItemPosition(int section, int position) {
|
|
return getAdapterPosition(section, position + 1);
|
|
}
|
|
|
|
// Overrides
|
|
/**
|
|
* Returns the total number of sections in the data set held by the adapter.
|
|
*
|
|
* @return The total number of section in this adapter.
|
|
*/
|
|
public int getSectionCount() {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of items in the <code>section</code>.
|
|
*
|
|
* @param section section to query
|
|
* @return The total number of items in the <code>section</code>.
|
|
*/
|
|
public int getSectionItemCount(int section) {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return the view type of the <code>section</code> header for the purposes
|
|
* of view recycling.
|
|
*
|
|
* <p>The default implementation of this method returns 0, making the assumption of
|
|
* a single view type for the headers. Unlike ListView adapters, types need not
|
|
* be contiguous. Consider using id resources to uniquely identify item view types.
|
|
*
|
|
* @param section section to query
|
|
* @return integer value identifying the type of the view needed to represent the header in
|
|
* <code>section</code>. Type codes need not be contiguous.
|
|
*/
|
|
public int getSectionHeaderViewType(int section) {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return the view type of the item at <code>position</code> in <code>section</code> for
|
|
* the purposes of view recycling.
|
|
*
|
|
* <p>The default implementation of this method returns 0, making the assumption of
|
|
* a single view type for the adapter. Unlike ListView adapters, types need not
|
|
* be contiguous. Consider using id resources to uniquely identify item view types.
|
|
*
|
|
* @param section section to query
|
|
* @param offset section position to query
|
|
* @return integer value identifying the type of the view needed to represent the item at
|
|
* <code>position</code> in <code>section</code>. Type codes need not be
|
|
* contiguous.
|
|
*/
|
|
public int getSectionItemViewType(int section, int offset) {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Returns true if header in <code>section</code> is sticky.
|
|
*
|
|
* @param section section to query
|
|
* @return true if <code>section</code> header is sticky.
|
|
*/
|
|
public boolean isSectionHeaderSticky(int section) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called when RecyclerView needs a new {@link HeaderViewHolder} of the given type to represent
|
|
* a header.
|
|
* <p>
|
|
* This new HeaderViewHolder should be constructed with a new View that can represent the headers
|
|
* of the given type. You can either create a new View manually or inflate it from an XML
|
|
* layout file.
|
|
* <p>
|
|
* The new HeaderViewHolder will be used to display items of the adapter using
|
|
* {@link #onBindHeaderViewHolder(HeaderViewHolder, int)}. Since it will be re-used to display
|
|
* different items in the data set, it is a good idea to cache references to sub views of
|
|
* the View to avoid unnecessary {@link View#findViewById(int)} calls.
|
|
*
|
|
* @param parent The ViewGroup into which the new View will be added after it is bound to
|
|
* an adapter position.
|
|
* @param headerType The view type of the new View.
|
|
*
|
|
* @return A new ViewHolder that holds a View of the given view type.
|
|
* @see #getSectionHeaderViewType(int)
|
|
* @see #onBindHeaderViewHolder(HeaderViewHolder, int)
|
|
*/
|
|
public abstract HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType);
|
|
|
|
/**
|
|
* Called when RecyclerView needs a new {@link ItemViewHolder} of the given type to represent
|
|
* an item.
|
|
* <p>
|
|
* This new ViewHolder should be constructed with a new View that can represent the items
|
|
* of the given type. You can either create a new View manually or inflate it from an XML
|
|
* layout file.
|
|
* <p>
|
|
* The new ViewHolder will be used to display items of the adapter using
|
|
* {@link #onBindItemViewHolder(ItemViewHolder, int, int)}. Since it will be re-used to display
|
|
* different items in the data set, it is a good idea to cache references to sub views of
|
|
* the View to avoid unnecessary {@link View#findViewById(int)} calls.
|
|
*
|
|
* @param parent The ViewGroup into which the new View will be added after it is bound to
|
|
* an adapter position.
|
|
* @param itemType The view type of the new View.
|
|
*
|
|
* @return A new ViewHolder that holds a View of the given view type.
|
|
* @see #getSectionItemViewType(int, int)
|
|
* @see #onBindItemViewHolder(ItemViewHolder, int, int)
|
|
*/
|
|
public abstract ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType);
|
|
|
|
/**
|
|
* Called by RecyclerView to display the data at the specified position. This method should
|
|
* update the contents of the {@link HeaderViewHolder#itemView} to reflect the header at the given
|
|
* position.
|
|
* <p>
|
|
* Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
|
|
* again if the position of the header changes in the data set unless the header itself is
|
|
* invalidated or the new position cannot be determined. For this reason, you should only
|
|
* use the <code>section</code> parameter while acquiring the
|
|
* related header data inside this method and should not keep a copy of it. If you need the
|
|
* position of a header later on (e.g. in a click listener), use
|
|
* {@link HeaderViewHolder#getAdapterPosition()} which will have the updated adapter
|
|
* position. Then you can use {@link #getAdapterPositionSection(int)} to get section index.
|
|
*
|
|
*
|
|
* @param viewHolder The ViewHolder which should be updated to represent the contents of the
|
|
* header at the given position in the data set.
|
|
* @param section The index of the section.
|
|
*/
|
|
public abstract void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section);
|
|
|
|
/**
|
|
* Called by RecyclerView to display the data at the specified position. This method should
|
|
* update the contents of the {@link ItemViewHolder#itemView} to reflect the item at the given
|
|
* position.
|
|
* <p>
|
|
* Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
|
|
* again if the position of the item changes in the data set unless the item itself is
|
|
* invalidated or the new position cannot be determined. For this reason, you should only
|
|
* use the <code>offset</code> and <code>section</code> parameters while acquiring the
|
|
* related data item inside this method and should not keep a copy of it. If you need the
|
|
* position of an item later on (e.g. in a click listener), use
|
|
* {@link ItemViewHolder#getAdapterPosition()} which will have the updated adapter
|
|
* position. Then you can use {@link #getAdapterPositionSection(int)} and
|
|
* {@link #getItemSectionOffset(int, int)}
|
|
*
|
|
*
|
|
* @param viewHolder The ViewHolder which should be updated to represent the contents of the
|
|
* item at the given position in the data set.
|
|
* @param section The index of the section.
|
|
* @param offset The position of the item within the section.
|
|
*/
|
|
public abstract void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset);
|
|
|
|
// Notify
|
|
/**
|
|
* Notify any registered observers that the data set has changed.
|
|
*
|
|
* <p>There are two different classes of data change events, item changes and structural
|
|
* changes. Item changes are when a single item has its data updated but no positional
|
|
* changes have occurred. Structural changes are when items are inserted, removed or moved
|
|
* within the data set.</p>
|
|
*
|
|
* <p>This event does not specify what about the data set has changed, forcing
|
|
* any observers to assume that all existing items and structure may no longer be valid.
|
|
* LayoutManagers will be forced to fully rebind and relayout all visible views.</p>
|
|
*
|
|
* <p><code>RecyclerView</code> will attempt to synthesize visible structural change events
|
|
* for adapters that report that they have {@link #hasStableIds() stable IDs} when
|
|
* this method is used. This can help for the purposes of animation and visual
|
|
* object persistence but individual item views will still need to be rebound
|
|
* and relaid out.</p>
|
|
*
|
|
* <p>If you are writing an adapter it will always be more efficient to use the more
|
|
* specific change events if you can. Rely on <code>notifyDataSetChanged()</code>
|
|
* as a last resort.</p>
|
|
*
|
|
* @see #notifySectionDataSetChanged(int)
|
|
* @see #notifySectionHeaderChanged(int)
|
|
* @see #notifySectionItemChanged(int, int)
|
|
* @see #notifySectionInserted(int)
|
|
* @see #notifySectionItemInserted(int, int)
|
|
* @see #notifySectionItemRangeInserted(int, int, int)
|
|
* @see #notifySectionRemoved(int)
|
|
* @see #notifySectionItemRemoved(int, int)
|
|
* @see #notifySectionItemRangeRemoved(int, int, int)
|
|
*/
|
|
public void notifyAllSectionsDataSetChanged() {
|
|
calculateSections();
|
|
notifyDataSetChanged();
|
|
}
|
|
|
|
public void notifySectionDataSetChanged(int section) {
|
|
calculateSections();
|
|
if (mSections == null) {
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
notifyItemRangeChanged(sectionObject.position, sectionObject.length);
|
|
}
|
|
}
|
|
|
|
public void notifySectionHeaderChanged(int section) {
|
|
calculateSections();
|
|
if (mSections == null) {
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
notifyItemRangeChanged(sectionObject.position, 1);
|
|
}
|
|
}
|
|
|
|
public void notifySectionItemChanged(int section, int position) {
|
|
calculateSections();
|
|
if (mSections == null) {
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
|
|
if (position >= sectionObject.itemNumber) {
|
|
throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber);
|
|
}
|
|
|
|
notifyItemChanged(sectionObject.position + position + 1);
|
|
}
|
|
}
|
|
|
|
public void notifySectionInserted(int section) {
|
|
calculateSections();
|
|
if (mSections == null) {
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
notifyItemRangeInserted(sectionObject.position, sectionObject.length);
|
|
}
|
|
}
|
|
|
|
public void notifySectionItemInserted(int section, int position) {
|
|
calculateSections();
|
|
if (mSections == null) {
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
|
|
if (position < 0 || position >= sectionObject.itemNumber) {
|
|
throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber);
|
|
}
|
|
|
|
notifyItemInserted(sectionObject.position + position + 1);
|
|
}
|
|
}
|
|
|
|
public void notifySectionItemRangeInserted(int section, int position, int count) {
|
|
calculateSections();
|
|
if (mSections == null) {
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
|
|
if (position < 0 || position >= sectionObject.itemNumber) {
|
|
throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber);
|
|
}
|
|
if (position + count > sectionObject.itemNumber) {
|
|
throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber);
|
|
}
|
|
|
|
notifyItemRangeInserted(sectionObject.position + position + 1, count);
|
|
}
|
|
}
|
|
|
|
public void notifySectionRemoved(int section) {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
calculateSections();
|
|
notifyItemRangeRemoved(sectionObject.position, sectionObject.length);
|
|
}
|
|
}
|
|
|
|
public void notifySectionItemRemoved(int section, int position) {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
|
|
if (position < 0 || position >= sectionObject.itemNumber) {
|
|
throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber);
|
|
}
|
|
|
|
calculateSections();
|
|
notifyItemRemoved(sectionObject.position + position + 1);
|
|
}
|
|
}
|
|
|
|
public void notifySectionItemRangeRemoved(int section, int position, int count) {
|
|
if (mSections == null) {
|
|
calculateSections();
|
|
notifyAllSectionsDataSetChanged();
|
|
}
|
|
else {
|
|
final Section sectionObject = mSections.get(section);
|
|
|
|
if (position < 0 || position >= sectionObject.itemNumber) {
|
|
throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber);
|
|
}
|
|
if (position + count > sectionObject.itemNumber) {
|
|
throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber);
|
|
}
|
|
|
|
calculateSections();
|
|
notifyItemRangeRemoved(sectionObject.position + position + 1, count);
|
|
}
|
|
}
|
|
}
|