RecyclerView双向无限滚动,可根据需要加载数据

时间:2018-01-23 11:07:22

标签: android android-recyclerview

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

此类实施的一个示例是Skype / Telegram聊天记录。当您打开聊天时,您将进入未读消息列表的开头,只要您开始滚动聊天记录,他们就会按需加载数据。

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

4 个答案:

答案 0 :(得分:0)

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

答案 1 :(得分:0)

我知道这是一个迟到的答案,但是当问题首次发布时,我认为github上已经有了实现,遗憾的是我找不到任何东西,并且有点忙于其他东西和回购被推回去。

所以我提出了解决问题的方法。您需要延长RecyclerView.Adapter并将自己的开始和结束位置保持为data adapterMap<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);,仅在顶部添加项目。

答案 2 :(得分:0)

正确的方法是使用Paging Library
在此之前,我在制作必须进行双向滚动的排行榜时遇到了同样的问题。我使用<bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="myDataSource"/> </bean> 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);
    }
};

这有效(您必须处理所有必须禁用滚动侦听器的情况),但是如果可以的话,我建议您使用Google的分页库。

答案 3 :(得分:0)

只需使用以下代码:

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();
      }
}