【问题标题】:RecyclerView two-way endless scroll with loading data on demandRecyclerView 双向无限滚动,按需加载数据
【发布时间】:2018-07-02 04:23:33
【问题描述】:

有很多关于如何使用EndlessScroll 和使用RecyclerView 按需加载数据的信息。但是,它仅支持在一个方向上滚动和加载数据。在我们的项目中,我们需要能够加载任意部分的数据,并允许用户在任一方向(向上和向下)滚动并在两个方向上按需加载数据。换句话说,每次用户滚动到结束时 - 在历史记录结束时加载数据。并且每次用户滚动到开始 - 在历史开始时加载数据

这种实现的一个例子是 Skype/Telegram 聊天记录。当您打开聊天时,您会到达未读消息列表的开头,只要您开始滚动聊天记录,它们就会按需加载数据。

RecyclerView 的问题是它使用偏移位置来处理项目和视图;难以将加载的数据提供给适配器并通知位置和计数的变化。当我们滚动到历史记录的开头时,我们无法在 -1 到 -n 的位置插入数据。 有没有人找到解决方案?即时更新物品的位置?

【问题讨论】:

  • 嗨,你找到答案了吗?我正在努力解决同样的问题

标签: android android-recyclerview


【解决方案1】:

我知道这是一个迟到的答案,但是当这个问题第一次发布时,我认为 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's 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's 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}'s data adapter is bounded to
         * the view.
         * Except the first time.
         * */
        void onTopReached();
        /**
         * To be called when the last item of the {@link RecyclerView}'s 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);只在顶部添加项目。

【讨论】:

  • @df778899 是的,这是我最初在这篇文章中链接的 git 存储库,后来我删除了它,因为我不想煽动点击/访问我自己的存储库。
【解决方案2】:

双向滚动时,您可以检查第一个和最后一个可见项目。基于此,您将能够对数据进行分页。

【讨论】:

  • 这里的问题是如何将加载的数据提供给适配器并通知位置和计数的变化。
【解决方案3】:

正确的做法是使用Paging Library
在此之前,我在制作必须具有双向滚动的排行榜时遇到了同样的问题。我通过使用RecyclerView.OnScrollListenerLinearLayoutManager 提供的一些方法解决了这个问题。

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 的分页库。

【讨论】:

    【解决方案4】:

    只需使用以下代码:

    LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false);
    mRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
          @Override
          public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
               if((newState==RecyclerView.SCROLL_STATE_IDLE)&&(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0)&&(linearLayoutManager.findLastCompletelyVisibleItemPosition()!=linearLayoutManager.getItemCount()-1)){
                        Toast.makeText(ChatPageActivity.this, "start", Toast.LENGTH_SHORT).show();
          }
    }
    

    【讨论】:

      猜你喜欢
      • 2018-05-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-19
      • 1970-01-01
      相关资源
      最近更新 更多