RecyclerView double-sided endless scroll with data loading on request

There is a lot of information on how to use EndlessScrolland load data on demand using RecyclerView. However, it only supports scrolling and loading data in one direction. In our project, we need to be able to load an arbitrary part of the data and allow the user to scroll in any of the directions (up and down) and load data on demand in both directions. In other words, every time the user scrolls to the end - loads data at the end of the story. And each time the user scrolls to the beginning - loads data at the beginning of the story

An example of such an implementation is the Skype / Telegram chat history. When you open a chat, you get to the top of the list of unread messages, and while you start scrolling through the chat history, they download data by request.

The problem with RecyclerViewis that it uses an offset position to address elements and views; which makes it difficult to provide downloaded data to the adapter and notifies about changes in positions and counting. When we move to the beginning of the story, we cannot insert data at positions from -1 to -n. Has anyone found a solution for this? Update item positions on the fly?

+7
source share
3 answers

. , .

0

, , , , github , , , .

, . RecyclerView.Adapter data adapter (Map<Integer, DataItem>).

TwoWayEndlessAdapter.java

/**
 * The {@link TwoWayEndlessAdapter} class provides an implementation to manage two end data
 * insertion into a {@link RecyclerView} easy by handling all of the logic within.
 * <p>To implement a TwoWayEndlessAdapter simply extend from it, provide the class type parameter
 * of the data type and <code>Override onBindViewHolder(ViewHolder, DataItem, int)</code> to bind
 * the view to.</p>
 *
 * @param <DataItem> A class type that can used by the data adapter.
 * @version 1.0.0
 * @author Abbas
 * @see android.support.v7.widget.RecyclerView.Adapter
 * @see TwoWayEndlessAdapterImp
 */

public abstract class TwoWayEndlessAdapter<VH extends RecyclerView.ViewHolder, DataItem> extends RecyclerView.Adapter<VH> {

    /*
    * Data Adapter Container.
    * */
    protected List<DataItem> data;

    private Callback mEndlessCallback = null;

    /*
    * Number of items before the last to get the lazy loading callback to load more items.
    * */
    private int bottomAdvanceCallback = 0;

    private boolean isFirstBind = true;

    /**
     * @param callback A listener to set if want to receive bottom and top reached callbacks.
     * @see TwoWayEndlessAdapter.Callback
     */
    public void setEndlessCallback(Callback callback)
    {
        mEndlessCallback = callback;
    }

    /**
     * Appends the provided list at the bottom of the {@link RecyclerView}
     *
     * @param bottomList The list to append at the bottom of the {@link RecyclerView}
     */
    public void addItemsAtBottom(ArrayList<DataItem> bottomList)
    {
        if (data == null) {
            throw new NullPointerException("Data container is `null`. Are you missing a call to setDataContainer()?");
        }

        if (bottomList == null || bottomList.isEmpty()) {
            return;
        }

        int adapterSize = getItemCount();

        data.addAll(adapterSize, bottomList);

        notifyItemRangeInserted(adapterSize, adapterSize + bottomList.size());
    }

    /**
     * Prepends the provided list at the top of the {@link RecyclerView}
     *
     * @param topList The list to prepend at the bottom of the {@link RecyclerView}
     */
    public void addItemsAtTop(ArrayList<DataItem> topList)
    {
        if (data == null) {
            throw new NullPointerException("Data container is `null`. Are you missing a call to setDataContainer()?");
        }

        if (topList == null || topList.isEmpty()) {
            return;
        }

        Collections.reverse(topList);
        data.addAll(0, topList);

        notifyItemRangeInserted(0, topList.size());
    }

    /**
     * To call {@link TwoWayEndlessAdapter.Callback#onBottomReached()} before the exact number of items to when the bottom is reached.
     * @see this.bottomAdvanceCallback
     * @see Callback
     * */
    public void setBottomAdvanceCallback(int bottomAdvance)
    {
        if (bottomAdvance < 0) {
            throw new IndexOutOfBoundsException("Invalid index, bottom index must be greater than 0");
        }

        bottomAdvanceCallback = bottomAdvance;
    }

    /**
     * Provide an instance of {@link Map} where the data will be stored.
     * */
    public void setDataContainer(List<DataItem> data)
    {
        this.data = data;
    }

    /**
     * Called by RecyclerView to display the data at the specified position. This method should
     * update the contents of the {@link RecyclerView.ViewHolder#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>position</code> parameter 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 RecyclerView.ViewHolder#getAdapterPosition()} which
     * will have the updated adapter position.
     *
     * Any class that extends from {@link TwoWayEndlessAdapter} should not Override this method but
     * should Override {@link #onBindViewHolder(VH, DataItem, int)} instead.
     *
     * @param holder The ViewHolder which should be updated to represent the contents of the
     *        item at the given position in the data set.
     * @param position The position of the item within the adapter data set.
     */
    @Override
    public void onBindViewHolder(VH holder, int position)
    {
        EndlessLogger.logD("onBindViewHolder() for position : " + position);

        onBindViewHolder(holder, data.get(position), position);

        if (position == 0 && !isFirstBind) {
            notifyTopReached();
        }
        else if ((position + bottomAdvanceCallback) >= (getItemCount() - 1)) {
            notifyBottomReached();
        }

        isFirstBind = false;
    }

    /**
     * Called by {@link TwoWayEndlessAdapter} to display the data at the specified position. This
     * method should update the contents of the {@link RecyclerView.ViewHolder#itemView} to reflect
     * the item at the given position.
     * <p>
     * Note that unlike {@link android.widget.ListView}, {@link TwoWayEndlessAdapter} 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>position</code> parameter while acquiring/verifying 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 RecyclerView.ViewHolder#getAdapterPosition()}
     * which will have the updated adapter position.
     *
     * Any class that extends from {@link TwoWayEndlessAdapter} must Override this method.
     *
     * @param holder The ViewHolder which should be updated to represent the contents of the
     *               item at the given position in the data set.
     * @param data The data class object associated with the corresponding position which contains
     *            the updated content that represents the item at the given position in the data
     *            set.
     * @param position The position of the item within the adapter data set.
     */
    public abstract void onBindViewHolder(VH holder, DataItem data, int position);

    /**
     * Sends the {@link Callback#onTopReached} callback if provided.
     * */
    protected void notifyTopReached()
    {
        Handler handler = new Handler(Looper.getMainLooper());

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (mEndlessCallback != null) {
                    mEndlessCallback.onTopReached();
                }
            }
        }, 50);

    }

    /**
     * Sends the {@link Callback#onBottomReached} callback if provided.
     * */
    protected void notifyBottomReached()
    {
        Handler handler = new Handler(Looper.getMainLooper());

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (mEndlessCallback != null) {
                    mEndlessCallback.onBottomReached();
                }
            }
        }, 50);
    }

    /**
     * The {@link TwoWayEndlessAdapter.Callback} class provides an interface notify when bottom or
     * top of the list is reached.
     */
    public interface Callback {
        /**
         * To be called when the first item of the {@link RecyclerView} data adapter is bounded to
         * the view.
         * Except the first time.
         * */
        void onTopReached();
        /**
         * To be called when the last item of the {@link RecyclerView} data adapter is bounded to
         * the view.
         * Except the first time.
         * */
        void onBottomReached();
    }
}

.

TwoWayEndlessAdapterImp.java

public class TwoWayEndlessAdapterImp<VH extends RecyclerView.ViewHolder> extends TwoWayEndlessAdapter<VH, ValueItem> {

    @Override
    public int getItemViewType(int position)
    {
        return R.layout.item_layout;
    }

    @Override
    public VH onCreateViewHolder(ViewGroup parent, int viewType)
    {
        View itemViewLayout = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);

        switch (viewType)
        {
            case R.layout.item_layout:

                return (VH) new ItemLayoutViewHolder(itemViewLayout);

            default:
                return null;
        }
    }

    @Override
    public void onBindViewHolder(VH holder, ValueItem item, int position)
    {
        switch (getItemViewType(position)) {
            case R.layout.item_layout:
                ItemLayoutViewHolder viewHolder = (ItemLayoutViewHolder) holder;

                viewHolder.textView.setText(item.data);
                break;
        }
    }

    @Override
    public int getItemCount()
    {
        return data == null ? 0 : data.size();
    }
}

TwoWayEndlessAdapter

TwoWayEndlessAdapterImp endlessAdapter = new TwoWayEndlessAdapterImp<>();
endlessAdapter.setDataContainer(new ArrayList<DataItem>());
endlessAdapter.setEndlessCallback(this);

, addItemsAtBottom(list);, addItemsAtTop(list); .

0

- .
, . , RecyclerView.OnScrollListener , LinearLayoutManager.

private LinearLayoutManager mLinearLayoutManager;
private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        if (mLinearLayoutManager.findLastVisibleItemPosition() == mFullLeaderboardAdapter.getItemCount() - 1) {
            //GET DATA HERE
        } else if (mLinearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
            //GET DATA HERE
        }
        super.onScrolled(recyclerView, dx, dy);
    }
};

, , ( , ). , :

public void addItems(List<UserLeaderboard> itemList) {
    if (mItemList == null) {
        mItemList = new ArrayList<>();
    }

    for (int i = 0; i < itemList.size(); i++) {
        UserLeaderboard user = itemList.get(i);
        if (!mItemList.contains(user)) {
            mItemList.add(user);
        }
    }
    Collections.sort(mItemList, (u1, u2) -> u1.getPosition() - u2.getPosition());
    notifyDataSetChanged();
}

( , ), , Google.

0

Source: https://habr.com/ru/post/1692574/


All Articles