RecyclerView底部刷新实现具体解释

关于RecyclerView底部刷新实现的文章已经非常多了,但大都仅仅介绍了其基本原理和框架,对当中的非常多细节没有交代,无法直接使用。

本文会着重介绍RecyclerView底部刷新实现的一些细节处理。

1. 顶部刷新和底部刷新

顶部刷新和底部刷新都是列表中两种常见的交互方式。顶部刷新通常相应更新数据,更新的数据会替换掉当前数据。

而底部刷新则相应获取很多其它的数据,很多其它的数据会加入在当前数据的后面。

顶部刷新和底部刷新在其它文章中很多其它的被称为下拉刷新和上拉载入很多其它,只是个人并不喜欢这样的称谓。每次提及上拉和下拉时都会感觉非常困惑。须要思考一下上拉和下拉究竟相应哪个操作。所以这里将这两种操作称为顶部刷新和底部刷新。当然假设读者没有这个困扰。认为非常easy区分上拉和下拉,不烦还是延续这样的称谓。

本文仅仅会介绍底部刷新。对顶部刷新会在后面的文章中再介绍。

2. RecyclerView底部刷新的原理

RecyclerView底部刷新的原理非常简单,就是在RecyclerView最底部加入一个表示载入中的View,然后监听RecyclerView的滑动事件,当RecyclerView滑动时。推断是否滑动到了RecyclerView的底部,也就是最后一个载入中的View是否可见,假设滑动到了RecyclerView底部,则运行底部刷新操作。获取很多其它数据。最后当获取很多其它数据完毕后,更新RecyclerView的Adapter。

3. RecyclerView底部刷新的一般实现

依据上述RecyclerView底部刷新的实现原理,能够知道RecyclerView底部刷新实际上包括例如以下步骤。注意这里的步骤并不代表代码的书写顺序,它很多其它的表示的是代码运行的顺序。

  1. 为RecyclerView底部加入一个表示载入中的View
  2. 设置RecyclerView的滑动事件监听,在滑动过程中。依据底部View是否可见,决定是否运行底部刷新操作
  3. 运行底部刷新时,获取很多其它数据
  4. 获取完数据后。通知Adapter更新RecyclerView

现分别介绍这4个步骤的实现。

在这之前,先限定一个约束条件。我们知道在使用RecyclerView时都须要调用其setLayoutManager()方法设置其LayoutManager。在V7包实现了三种类型的LayoutManager。即LinearLayoutManager。GridLayoutManager和StaggeredGridLayoutManager。这三种类型的LayoutManager在实现底部刷新时会有一些细节上的差异。为了简化描写叙述和方便理解。在这里介绍RecyclerView底部刷新的一般实现时,仅仅考虑LinearLayoutManager,对其它两种类型有差异的地方会在后文单独说明。

3.1 为RecyclerView底部加入一个表示载入中的View

表示载入中的View

这个表示载入中的View一般会使用一个居中显示的ProgressBar来表示。其布局例如以下。

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="40dp">

    <ProgressBar
        style="?android:attr/progressBarStyle"
        android:layout_gravity="center"
        android:layout_width="30dp"
        android:layout_height="30dp"/>

</FrameLayout>

只是这并不是是强制要求。其详细样式能够依据须要自由定义。

后文会将这个表示载入中的View称为底部刷新View(Bottom Refresh View)。

为RecyclerView加入底部刷新View

为RecyclerView加入底部刷新View通常是通过将底部刷新View作为RecyclerView的Item来实现的。

为此须要改写RecyclerView Adapter的下面几个方法。

  1. getItemCount

    RecyclerView Adapter的getItemCount方法返回的是item的数量,既然要将底部刷新View作为RecyclerView的Item加入到RecyclerView中,就须要在原有item数量基础上加1。

    比如:

    @Override
    public int getItemCount() {
        return mList.size() + 1;
    }
  2. getItemViewType

    RecyclerView Adapter的getItemViewType方法返回的是item的类型。为了将底部刷新View相应的item和其它item区分开。须要将底部刷新View作为一个单独的类型返回。

    比如:

    @Override
    public int getItemViewType(int position) {
        if (position < mList.size()) {
            return TYPE_NORMAL_ITEM;
        } else {
            return TYPE_BOTTOM_REFRESH_ITEM;
        }
    }
  3. onCreateViewHolder

    RecyclerView Adapter的onCreateViewHolder方法用来创建ViewHolder。这里首先须要为底部刷新View定义一个ViewHolder。然后依据item的类型来决定要创建哪个ViewHolder。

    比如:

    // 定义底部刷新View相应的ViewHolder
    private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {
        BottomViewHolder(View itemView) {
            super(itemView);
        }
    }
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 假设是底部刷新View,则载入底部刷新View布局,并创建底部刷新View相应的ViewHolder
        if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);
            return new BottomRefreshViewHolder(view);
        }
        // 假设是其它类型的View,则依照正常流程创建普通的ViewHolder
        else {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_normal_item, parent, false);
            return new NormalViewHolder(view);
        }
    }
  4. onBindViewHolder

    RecyclerView Adapter的onBindViewHolder方法用来将ViewHolder和相应的数据绑定起来。因为底部刷新View并不须要绑定不论什么数据,所以这里不须要对底部刷新ViewHolder做特别的处理,仅仅须要推断下是否是底部刷新ViewHolder就能够了。

    比如:

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (!(holder instanceof BottomRefreshViewHolder)) {
            ...
        }
    }

完毕了上述步骤之后即为RecyclerView底部加入一个底部刷新View

3.2 滑动事件处理

设置滑动事件监听

RecyclerView提供了addOnScrollListener()方法来设置滑动事件监听,仅仅须要将监听滑动事件的RecyclerView.OnScrollListener对象作为參数传递进去就可以。

比如:

addOnScrollListener(onScrollListener);

onScrollStateChanged()和onScrolled()

RecyclerView.OnScrollListener是一个抽象类,它包括两个方法onScrollStateChanged()和onScrolled()。

onScrollStateChanged()方法会在每次滑动状态发生改变时调用。

比如,由精巧状态变为滑动状态,或者由滑动状态变为精巧状态时,onScrollStateChanged()方法都会被调用。

onScrolled()方法会在RecyclerView滑动时被调用。即使手指离开了屏幕,仅仅要RecyclerView仍然在滑动onScrolled()就会被不断调用。

理论上来说。我们既能够将推断底部View是否可见和运行底部刷新操作的过程放到onScrollStateChanged()方法中运行,也能够将其放到onScrolled()方法中运行。

但放到不同方法中运行在用户体验上会产生一些不同。

假设将推断底部View是否可见和运行底部刷新操作的过程放到onScrollStateChanged()方法中运行,意味着是以一次滑动过程的终于状态来决定是否要运行底部刷新。假设在一次滑动过程中间,底部View已经可见,可是终于停下来的时候底部View是不可见的,那么将不会运行底部刷新操作。

假设将推断底部View是否可见和运行底部刷新操作的过程放到onScrolled()方法中运行,意味着仅仅要在一次滑动过程中间底部View可见。那么将会立马触发底部刷新操作。

观察大部分的APP。都是仅仅要出现底部载入中View。就会開始运行底部刷新操作,这也和一般用户的认知相一致。所以,一般我们都会将推断底部View是否可见和运行底部刷新操作的过程放到onScrolled()方法中运行。可是onScrollStateChanged()方法仍然是实用的。有些辅助的逻辑会放到当中来运行。详细哪些逻辑须要放到onScrollStateChanged()方法中会在文章后面提到。

推断底部刷新View是否可见

推断底部刷新View是否可见是实现RecyclerView底部刷新功能的关键。

只是幸好它的实现并不复杂。

在LinearLayoutManager中提供了一个方法能够获取到当前最后一个可见的item在RecyclerView Adapter中的位置,假设这个位置恰好等于RecyclerView Adapter中item的数量减1。那么就表示底部刷新View已经可见了。

这也非常easy理解,比如RecyclerView Adapter中有55个item,因为Adapter中的位置都是从0開始的,所以这55个item的位置就是从0到54。最后一个item(也就是底部刷新View相应的item)的位置是54。假设当前最后一个可见的item位置为54,那么就表示底部刷新View是可见的。

对LinearLayoutManager,能够调用其findLastVisibleItemPosition()方法来获取当前最后一个可见的item在RecyclerView Adapter中的位置。

演示样例代码例如以下。

private int getLastVisibleItemPosition() {
    RecyclerView.LayoutManager manager = getLayoutManager();
    if (manager instanceof LinearLayoutManager) {
        return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
    }
    return NO_POSITION;
}

private boolean isBottomViewVisible() {
    int lastVisibleItem = getLastVisibleItemPosition();
    return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;
}

运行底部刷新操作

将上述几个步骤组合在一起就能够得到完整的滑动事件处理过程。演示样例代码例如以下。

RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        if (isBottomViewVisible()) {
            requestMoreData();
        }
    }
};
addOnScrollListener(onScrollListener);

3.3 获取很多其它数据

获取数据的流程一般都是通过调用约定的接口从服务端获取数据,这属于业务逻辑。这里不做介绍了。

3.4 更新RecyclerView

获取数据一般都是异步过程,在获取数据完毕后,调用RecyclerView Adapter的相关方法更新RecyclerView。

因为是获取很多其它数据,所以一般能够调用notifyItemInserted()或者notifyItemRangeInserted()来更新RecyclerView。

至此。RecyclerView底部刷新的基本实现就已经完毕了。

4. 底部刷新功能的封装

上述底部刷新功能的实现,包括了两部分的改动,一部分是对RecyclerView自身的一些设置,比如设置滑动事件监听,推断底部刷新View是否可见等。

另外一部分是对RecyclerView Adapter的改动,也就是为RecyclerView加入底部刷新View。因为一个app中通常都会有多个界面须要实现底部刷新功能,假设每一个要实现底部刷新功能的界面都这样实现一遍,实在是太麻烦,也会使原本的代码变得复杂和臃肿。因此,须要将上述底部刷新功能的实现封装在一起。

对第一部分RecyclerView自身的一些设置,能够非常easy的通过继承RecyclerView来实现封装,然后在代码和xml中使用这个继承之后的RecyclerView就可以。对第二部分RecyclerView Adapter的改动要麻烦一些,因为不同的列表都须要定义单独的Adapter,在这些Adapter中都须要重写getItemCount()。getItemViewType()这些方法。所以不能简单的通过继承RecyclerView Adapter,然后各个列表的Adapter再继承自这个改动后的Adapter来解决。为了实现Adapter的封装,须要实现一个内部的Adapter,然后用这个内部的Adapter包裹外部列表的Adapter来实现。

现分别对这两部分的封装过程进行介绍。

4.1 RecyclerView的封装

对RecyclerView的封装仅仅须要实现一个类继承自RecyclerView,将底部刷新功能对RecyclerView的改动放到这个类中就可以。

演示样例代码例如以下。

public class XRecyclerView extends RecyclerView {

    private OnBottomRefreshListener mBottomRefreshListener;
    private RecyclerView.OnScrollListener mOnScrollListener;
    private boolean mBottomRefreshable;

    public XRecyclerView(Context context) {
        super(context);
        init();
    }

    public XRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mBottomRefreshListener = null;
        mBottomRefreshable = false;
        mOnScrollListener = new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (isBottomViewVisible()) {
                    if (mBottomRefreshListener != null) {
                        mBottomRefreshListener.onBottomRefresh();
                    }
                }
            }
        };
    }

    private int getLastVisibleItemPosition() {
        RecyclerView.LayoutManager manager = getLayoutManager();
        if (manager instanceof LinearLayoutManager) {
            return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
        }
        return NO_POSITION;
    }

    private boolean isBottomViewVisible() {
        int lastVisibleItem = getLastVisibleItemPosition();
        return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;
    }

    public boolean isBottomRefreshable() {
        return mBottomRefreshable;
    }

    // 设置底部下拉刷新监听
    public void setOnBottomRefreshListener(OnBottomRefreshListener listener) {
        mBottomRefreshListener = listener;
        if (mBottomRefreshListener != null) {
            addOnScrollListener(mOnScrollListener);
            mBottomRefreshable = true;
        } else {
            removeOnScrollListener(mOnScrollListener);
            mBottomRefreshable = false;
        }
    }

    public interface OnBottomRefreshListener {
        void onBottomRefresh();
    }
}

这里的代码和上面介绍RecyclerView底部刷新的一般实现时的演示样例代码基本一致。

不同的是添加了一个OnBottomRefreshListener的接口类,setOnBottomRefreshListener()方法用来设置底部刷新事件的监听。

当须要运行底部刷新时,调用OnBottomRefreshListener的onBottomRefresh()方法,通知外部更新数据。此外,将设置滑动事件监听也放到setOnBottomRefreshListener()方法中。仅仅有设置了底部下拉刷新监听,才须要加入滑动事件监听,不然监听滑动事件是没有意义的。在setOnBottomRefreshListener()方法中还同意通过传入一个null对象,表示取消对底部下拉刷新的监听。mBottomRefreshable表示底部能否够刷新,它会后面Adapter中用到。

4.2 RecyclerView Adapter的封装

对RecyclerView Adapter的封装须要在继承的RecyclerView类再实现一个包裹的Adapter(WrapperAdapter),并又一次RecyclerView的setAdapter()方法,当在外部调用setAdapter()时会用WrapperAdapter包裹外部Adapter。

演示样例代码例如以下。

@Override
public void setAdapter(RecyclerView.Adapter adapter) {
    if (adapter != null) {
        WrapperAdapter wrapperAdapter = new WrapperAdapter(adapter);
        super.setAdapter(wrapperAdapter);
    }
}

private class WrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;

    /**
     * 被包裹的外部Adapter
     */
    private RecyclerView.Adapter mInnerAdapter;

    private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            notifyDataSetChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            super.onItemRangeChanged(positionStart, itemCount);
            notifyItemRangeChanged(positionStart, itemCount);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            notifyItemRangeInserted(positionStart, itemCount);
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            super.onItemRangeRemoved(positionStart, itemCount);
            notifyItemRangeRemoved(positionStart, itemCount);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            super.onItemRangeMoved(fromPosition, toPosition, itemCount);
            notifyItemRangeChanged(fromPosition, toPosition + itemCount);
        }
    };

    private WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {
        if (mInnerAdapter != null) {
            notifyItemRangeRemoved(0, mInnerAdapter.getItemCount());
            mInnerAdapter.unregisterAdapterDataObserver(dataObserver);
        }
        this.mInnerAdapter = adapter;
        mInnerAdapter.registerAdapterDataObserver(dataObserver);
        notifyItemRangeInserted(0, mInnerAdapter.getItemCount());
    }

    public boolean isLoadMoreView(int position) {
        return isBottomRefreshable() && position == getItemCount() - 1;
    }

    @Override
    public int getItemCount() {
        if (mInnerAdapter != null) {
            int itemCount = mInnerAdapter.getItemCount();
            if (isBottomRefreshable()) {
                return itemCount + 1;
            } else {
                return itemCount;
            }
        } else {
            return 0;
        }
    }

    @Override
    public int getItemViewType(int position) {
        if (isBottomRefreshable()) {
            if (mInnerAdapter != null) {
                int adapterCount = mInnerAdapter.getItemCount();
                if (position < adapterCount) {
                    return mInnerAdapter.getItemViewType(position);
                }
            }
            return TYPE_BOTTOM_REFRESH_ITEM;
        } else {
            if (mInnerAdapter != null) {
                return mInnerAdapter.getItemViewType(position);
            } else {
                return super.getItemViewType(position);
            }
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);
            return new BottomRefreshViewHolder(view);
        } else {
            return mInnerAdapter.onCreateViewHolder(parent, viewType);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (!(holder instanceof BottomRefreshViewHolder)) {
            if (mInnerAdapter != null) {
                int adapterCount = mInnerAdapter.getItemCount();
                if (position <adapterCount) {
                    mInnerAdapter.onBindViewHolder(holder, position);
                }
            }
        }
    }
}

/**
 * bottom refresh View相应的ViewHolder
 */
private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {
    BottomRefreshViewHolder(View itemView) {
        super(itemView);
    }
}

对这里的代码再稍作说明。

  1. 底部刷新View类型相应的整数值不能和外部Adapter已有的类型值反复,因为无法事先知道外部Adapter会定义哪些类型,所以这里为底部刷新View定义了一个相对来说比較特殊的数值Integer.MIN_VALUE + 1,但假设在实际使用时,发现外部Adapter也定义了这样的类型。则须要改动这个数值,或者改动外部Adapter的定义。

  2. 因为外部数据更新时都是调用外部Adapter的notify…方法来通知RecyclerView更新,所以这里定义了dataObserver对象,当外部Adapter的notify…方法被调用时。调用包裹的Adapter的notify…方法。

  3. 这里使用了RecyclerView封装时定义的isBottomRefreshable()方法。来推断是否须要加入底部刷新View。也就是说。假设外部没有调用setOnBottomRefreshListener()设置底部刷新监听,则isBottomRefreshable()将返回false,表示不须要运行底部刷新操作。因此也就不须要加入底部载入View。

5. 支持其它LayoutManager

如前所述,GridLayoutManager和StaggeredGridLayoutManager在实现底部刷新时会和LinearLayoutManager存在一些差异。

这里介绍怎样支持GridLayoutManager和StaggeredGridLayoutManager的底部刷新功能。

5.1 支持GridLayoutManager

GridLayoutManager是网格布局,同意在一行中有多列。每一列都是一个item。底部刷新View也是一个作为一个item加入到Adapter中。依照之前LinearLayoutManager的实现。它会被安排在最后一行的某一列上。不能占领整行。如图所看到的。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2NwYXQ=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

为了让底部刷新View能够占领整行,须要对GridLayoutManager对象进行设置。GridLayoutManager提供了setSpanSizeLookup()方法,在这里能够设置某个位置的item跨越多列。我们仅仅须要将底部刷新View跨越列数设置为GridLayoutManager的列数就可以。

为此,须要重写Adapter的onAttachedToRecyclerView()方法。

添加例如以下代码。

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);
    // 对Grid布局进行支持
    RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
    if (manager instanceof GridLayoutManager) {
        final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
        final GridLayoutManager.SpanSizeLookup lookup = gridLayoutManager.getSpanSizeLookup();
        gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return isLoadMoreView(position) ? gridLayoutManager.getSpanCount() : lookup.getSpanSize(position) ;
            }
        });
    }
}

5.2 支持StaggeredGridLayoutManager

StaggeredGridLayoutManager是瀑布流布局,它相同同意在一行中有多列,因此和GridLayoutManager一样。须要将底部刷新View设置为跨越多列。只是StaggeredGridLayoutManager并没有setSpanSizeLookup方法,假设要设置某个item跨越整行,须要调用item的LayoutParams的setFullSpan()方法。

为此。须要重写Adapter的onViewAttachedToWindow()方法,添加例如以下代码。

@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
    super.onViewAttachedToWindow(holder);
    if (holder instanceof BottomRefreshViewHolder) {
        // 支持瀑布流布局
        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
        if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
            ((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);
        }
    }
}

对StaggeredGridLayoutManager,完毕上述代码后,还须要做另外一处改动。

在先前介绍推断底部刷新View是否可见时,是通过LinearLayoutManager的findLastVisibleItemPosition()方法获取当前最后一个可见的item位置来推断的。

对StaggeredGridLayoutManager,并没有findLastVisibleItemPosition()方法,但StaggeredGridLayoutManager有一个findLastVisibleItemPositions()方法,它能够用来获取每一列的最后一个可见item的位置。通过将所有列最后一个可见item位置取最大值就能够得取到当前最后一个可见的item位置。

代码例如以下。

private int getLastVisibleItemPosition() {
    RecyclerView.LayoutManager manager = getLayoutManager();
    if (manager instanceof LinearLayoutManager) {
        return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
    } else if (manager instanceof StaggeredGridLayoutManager) {
        int positions[] = ((StaggeredGridLayoutManager) manager).findLastVisibleItemPositions(null);
        int max = NO_POSITION;
        for (int pos : positions) {
            if (pos > max) {
                max = pos;
            }
        }
        return max;
    }
    return NO_POSITION;
}

注意到。这里面仅仅有LinearLayoutManager和StaggeredGridLayoutManager两个分支,并没有GridLayoutManager。这是因为GridLayoutManager是继承自LinearLayoutManager的,所以它包括在了LinearLayoutManager这个分支中,对GridLayoutManager不须要在这里特别处理了。

有意思的是StaggeredGridLayoutManager尽管名字中包括了GridLayoutManager。但却并不是继承自GridLayoutManager。也没有继承自LinearLayoutManager,所以对StaggeredGridLayoutManager是须要单独处理的。

5.3 支持自己定义的LayoutManager

对自己定义的LayoutManager,假设是继承自LinearLayoutManager,GridLayoutManager或StaggeredGridLayoutManager,则仅仅须要依照上述说明,实现对GridLayoutManager和StaggeredGridLayoutManager的支持就能够了。

假设自己定义的LayoutManager是直接继承自RecyclerView.LayoutManager,则须要在自己定义的LayoutManager中实现获取当前可见的最后一个item位置的方法,此外,假设自己定义的LayoutManager支持一行放置多个item,还须要实现能够将某个item设置为跨越整行的方法。然后參照上述对GridLayoutManager和StaggeredGridLayoutManager的处理,在包裹的Adapter中加入相应的分支就可以。

6. 一次滑动过程中避免反复刷新

在设置滑动事件监听时,我们重写了RecyclerView.OnScrollListener对象的onScrolled()方法,在这里推断底部刷新View是否可见,假设底部刷新View可见,则运行底部刷新操作。

这里存在一个问题,设想手指正在屏幕上向上滑动,此时底部刷新View变得可见。于是触发底部刷新操作。可是手指并未立马离开屏幕,而是继续向下滑动,这时onScrolled()方法仍然会被连续调用,因为底部刷新View仍然可见。所以底部刷新操作也会被不断的触发。再设想另外一种情况,用户触发底部刷新操作后手指立马离开屏幕,但因为获取数据通常都是异步的过程。从触发底部刷新到获取到数据是须要一定时间的,假设在这段时间内。用户又又一次滑动了列表,并使得底部刷新View可见,这时底部刷新操作又会被再一次调用。须要注意的是,这两种情况尽管都是反复触发了底部刷新操作,可是存在一定差异。第一种情况,触发底部刷新操作后手指未离开屏幕,假设这时手指能够全然精巧的停留在屏幕上,不会触发新的底部刷新操作。等待一会之后外部完毕了数据获取。并更新了RecyclerView。这时手指继续滑动,使得底部刷新View又变得可见,这时仍然会触发底部刷新操作,所以另外一种情况并不能够涵盖第一种情况。

一般来说,我们并不希望在一次滑动过程中。触发多次底部刷新操作,也不希望在获取到数据前。再次运行底部刷新操作。

为了避免反复刷新,须要在onScrolled()中添加一些额外的处理,同一时候还须要利用到RecyclerView.OnScrollListener的另外一个方法onScrollStateChanged()。

演示样例代码例如以下。注意这里仅仅是列出了相关改动的地方。之前已经提到的部分就省略了。并不是完整的代码。

public class XRecyclerView extends RecyclerView {

    private boolean mBottomRefreshing;

    private void init() {
        mBottomRefreshing = false;
        mOnScrollListener = new RecyclerView.OnScrollListener() {

            private boolean mAlreadyRefreshed = false;

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (mAlreadyRefreshed) {
                        mAlreadyRefreshed = false;
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (isBottomRefreshing() || mAlreadyRefreshed) {
                    return;
                }
                if (isBottomViewVisible()) {
                    if (mBottomRefreshListener != null) {
                        mBottomRefreshListener.onBottomRefresh();
                        mBottomRefreshing = true;
                        mAlreadyRefreshed = true;
                    }
                }
            }
        };
    }

    public boolean isBottomRefreshing() {
        return mBottomRefreshing;
    }

    public void onBottomRefreshComplete() {
        mBottomRefreshing = false;
    }
}

在这段代码中主要是添加了两个变量。mBottomRefreshing和mAlreadyRefreshed。

mBottomRefreshing变量添加在继承的RecyclerView中,表示是否正在刷新。它初始值为false,在onScroll中,假设触发了一次刷新操作,则将其置为true。之后仅仅有在外部调用了onBottomRefreshComplete()才会再次置为false。在onScroll中,触发刷新操作之前添加了推断,假设它为true。则不会再运行刷新操作。这就杜绝了上述另外一种情况下的反复刷新,在获取数据完毕之前。是不能够再次触发刷新操作的。

可是同一时候要求外部在获取数据完毕后。调用这里的onBottomRefreshComplete()。通知RecyclerView数据已经获取好了。

mAlreadyRefreshed变量添加在实现RecyclerView.OnScrollListener接口的匿名内部类中,它表示本次滑动过程中是否已经运行过一次底部刷新操作了。假设触发了一次刷新操作,则将其置为true。在onScrollStateChanged()中,假设滑动状态变为停止滑动,且mAlreadyRefreshed为true,则将其重置为false。在onScroll中,触发刷新操作之前推断mAlreadyRefreshed是否为true,假设为true,则不运行刷新操作。

这样就能够避免在一次滑动过程中反复运行底部刷新操作。

7. 底部刷新触发过于敏感问题

在onScroll中。仅仅要底部刷新View一旦可见就会触发底部刷新操作,哪怕仅仅是可见一个像素也会立马触发。个人认为这样的触发机制过于敏感。用户体验不是非常好,尤其是如今用户非常多都使用wifi环境来上网。网速都非常快,一旦触发底部刷新,非常快就能获取到新的数据,然后RecyclerView被更新。导致底部刷新View非常难被用户观察到。

假设在onScroll中运行底部刷新操作之前添加推断,仅仅有底部刷新View可见到一定程度后。比如有一半可见时才会触发底部刷新操作。这样就能够避免这样的问题。只是这个问题并不会影响到整个功能。假设认为对用户体验影响不大,全然能够忽略问题。

演示样例代码例如以下。

相同的,这里仅仅是列出了相关改动的地方。

private void init() {
    mOnScrollListener = new RecyclerView.OnScrollListener() {
        private int mBottomViewVisibleDy = 0;
        private int mBottomViewHeight = 0;

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                boolean shouldHide = false;             // 是否须要隐藏bottom view
                // 假设之前还没有滑到指定的位置就停止了滑动,则将shouldHide置为true
                if (mBottomViewVisibleDy != 0) {
                    if (mBottomViewVisibleDy > 0) {
                        shouldHide = true;
                    }
                    mBottomViewVisibleDy = 0;
                }
                // 隐藏bottom view
                if (shouldHide) {
                    hideBottomView();
                }
            }
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if (isBottomViewVisible()) {
                // dy是本次调用onScrolled和上次调用onScrolled在y轴方向的偏移量,这里将bottom view可见之后的偏移量累加起来
                mBottomViewVisibleDy += dy;
                if (mBottomViewHeight == 0) {
                    View itemView = getLastVisibleItem();
                    if (itemView != null) {
                        mBottomViewHeight = itemView.getHeight();
                    }
                }
                // 假设bottom view可见之后的y轴偏移量大于bottom view高度的一半。则运行bottom refresh
                if (mBottomViewHeight != 0 && mBottomViewVisibleDy > mBottomViewHeight / 2) {
                    if (mBottomRefreshListener != null) {
                        mBottomRefreshListener.onBottomRefresh();
                        mBottomRefreshing = true;
                        mAlreadyRefreshed = true;
                    }
                    mBottomViewVisibleDy = 0;
                }
            } else {
                mBottomViewVisibleDy = 0;
            }
        }
    };
}

private View getLastVisibleItem() {
    int firstItemPosition = getFirstVisibleItemPosition();
    int lastItemPosition = getLastVisibleItemPosition();
    if (firstItemPosition != NO_POSITION && lastItemPosition != NO_POSITION) {
        return getLayoutManager().getChildAt(lastItemPosition - firstItemPosition);
    } else {
        return null;
    }
}

private int getFirstVisibleItemPosition() {
    RecyclerView.LayoutManager manager = getLayoutManager();
    if (manager instanceof LinearLayoutManager) {
        return ((LinearLayoutManager) manager).findFirstVisibleItemPosition();
    } else if (manager instanceof StaggeredGridLayoutManager) {
        int positions[] = ((StaggeredGridLayoutManager) manager).findFirstVisibleItemPositions(null);
        int min = Integer.MAX_VALUE;
        for (int pos : positions) {
            if (pos < min) {
                min = pos;
            }
        }
        return min;
    }
    return NO_POSITION;
}

// 假设bottom view是可见的,则依据bottom view 当前的位置和RecyclerView当前位置来决定要向上滑动的距离
private void hideBottomView() {
    if (isBottomViewVisible()) {
        View bottomView = getLastVisibleItem();
        if (bottomView != null) {
            int[] bottomViewLocation = new int[2];
            bottomView.getLocationInWindow(bottomViewLocation);
            int[] recyclerViewLocation = new int[2];
            getLocationInWindow(recyclerViewLocation);
            int recyclerViewHeight = getHeight();
            int offset = recyclerViewLocation[1] + recyclerViewHeight - bottomViewLocation[1];
            if (offset > 0) {
                scrollBy(0, -offset);
            }
        }
    }
}

在这段代码中,添加了mBottomViewVisibleDy变量,表示底部刷新View可见的高度大小,此外,添加了getLastVisibleItem()方法,用来获取最后一个可见的item。它主要是用来得究竟部刷新View的高度。还添加了hideBottomView()方法,它用来隐藏底部刷新View,因为在onScroll中添加了这层推断后,仅仅有底部刷新View可见到一定程度(这里是底部刷新View高度的一半)时才会运行底部刷新操作,假设没有滑到一半就停下来了,那么就须要手动将这显示出来一小半的底部刷新View隐藏起来。

8. 获取到数据后的处理

如前所述。在获取到数据后,须要外部调用onBottomRefreshComplete()。通知RecyclerView数据已经获取好了。在onBottomRefreshComplete()会将mBottomRefreshing置为false。可是仅仅这样处理是不够的。这里还须要做一个额外的操作,就是将底部刷新View隐藏起来。

当外部调用onBottomRefreshComplete()时,即表明本次刷新操作已经完毕,这时就不应当再让用户看到这个底部刷新View。

然而可能存在一些原因导致这时用户仍然能够看究竟部刷新View。这可能是因为获取到的数据量太少,RecyclerView填充新的数据后,也无法将底部刷新View挤到看不见的地方,也可能是因为外部在获取数据后添加了去重的操作。新获取的数据所有都在已经获取的数据里面,导致数据项没有发生变化。不管是哪种情况都不应当再让用户看究竟部刷新View,因此须要在onBottomRefreshComplete()中将底部刷新View隐藏起来。

演示样例代码例如以下。

private boolean mShouldHideAfterScrollIdle;
private void init() {
    mOnScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                boolean shouldHide = false;             // 是否须要隐藏bottom view
                // 假设须要隐藏bottom view,则将shouldHide置为true
                if (mShouldHideAfterScrollIdle) {
                    shouldHide = true;
                    mShouldHideAfterScrollIdle = false;
                }
                // 隐藏bottom view
                if (shouldHide) {
                    hideBottomView();
                }
            }
        }
    };
}
public void onBottomRefreshComplete() {
    mBottomRefreshing = false;
    // 假设当前没有在滑动状态。则直接隐藏
    // 假设当前在滑动状态。则等待滑动停止后再隐藏
    if (getScrollState() == SCROLL_STATE_IDLE) {
        hideBottomView();
        mShouldHideAfterScrollIdle = false;
    } else {
        mShouldHideAfterScrollIdle = true;
    }
}

这里借用了之前的hideBottomView()方法来隐藏底部刷新View,同一时候还须要和滑动状态和onScrollStateChanged()方法相结合。

9. 获取数据失败后的处理

因为获取数据通常都是联网获取。联网获取总是会有获取失败的可能。假设数据获取失败,那么须要做相应的处理。

一般来说有两种处理方式。

1. 外部得知数据获取失败后,显示出错信息。调用onBottomRefreshComplete()。通知RecyclerView刷新完毕,RecyclerView内部仍然是之前的处理流程。将mBottomRefreshing置为false,然后隐藏底部刷新View。

2. RecyclerView添加一个方法onBottomRefreshFailed()。外部得知数据获取失败后,调用onBottomRefreshFailed(),在onBottomRefreshFailed()方法中。将mBottomRefreshing置为false,同一时候在底部刷新View中显示载入出错的信息。

採用方法2须要对底部刷新View进行改造,将其设置为一个TextView和ProgressBar的组合。平时显示ProgressBar,隐藏TextView,在获取数据失败后,改变其状态,隐藏ProgressBar。显示TextView。将TextView文本设置为“载入数据失败”之类的信息。同一时候。原先的滑动究竟部刷新View运行刷新的机制也须要改动。改为点击后刷新。

对方法2尽管须要的改动较多,但整体思路是非常明白的。这里就不给出实现代码了。

10. 所有数据获取完毕的处理

获取数据的结果在获取成功时还存在还有一种特殊情况,就是已经获取到了所有的数据,后面已经没有很多其它数据了。

为了处理这样的情况,能够在RecyclerView中添加setBottomRefreshable()方法,当数据所有获取完毕后,调用此方法,通知RecyclerView已经不能够再刷新了。在setBottomRefreshable()中有两种处理方式。

  1. 去掉底部刷新View。这仅仅须要将mBottomRefreshable置为false就可以,因为在Adapter有推断此变量的状态(isBottomRefreshable())。假设mBottomRefreshable为false,则不会加入底部刷新View。

  2. 底部刷新View中显示没有很多其它的信息。

对方法2,相同须要对底部刷新View进行改造,改造方式和之前介绍获取数据失败后的处理方式2一样。将其设置为一个TextView和ProgressBar的组合。当RecyclerView得知数据已经所有获取完毕后,隐藏ProgressBar。显示TextView。

将TextView文本设置为“没有很多其它了”之类的信息。

同一时候,去掉原先的滑动究竟部刷新View运行刷新的机制。非常多时候数据是在不断变化的。数据已经所有获取完毕仅仅是表示当前的状态,可能过一会之后就会有新的数据产生,这时能够将底部刷新View置为点击后刷新。

假设确实不会产生新的数据了,也能够不设置点击刷新操作。

11. 当前数据不足一屏的处理

考虑这样一种情况。当填充完数据后,当前RecyclerView显示不足一屏。因为底部刷新View是作为最后一个item加入的,所以这时底部刷新View将会直接可见。同一时候因为RecyclerView不足一屏,所以它不能滑动,onScroll也就不会被运行,数据更新操作也就无法触发。

要解决问题。通常也有两种方案。

1. 外部第一次获取数据时,尽量多获取一些数据。确保RecyclerView能够填满一屏。尽管这个方法看起来是什么也没做。将问题解决责任推给外部实现。

但实际上大多数情况都能够採用这样的方式。一般来说。即使要显示的item是非常简单的,比如仅仅有一个icon和一行文本,也仅仅须要十多个就能够填满一屏。外部获取数据时全然能够将一次获取数量设置为20或者很多其它,这样就能够保证RecyclerView填满一屏。而一次获取20个数据大多数情况全然不会造成负担。所以将数据不足一屏的问题推给外部,是全然合理的。

2. 相同是改造底部刷新View。将其设置为一个TextView和ProgressBar的组合。依据RecyclerView能否够滑动,决定是显示TextView还是ProgressBar。假设RecyclerView超过一屏,则显示ProgressBar,设置滑动触发刷新,假设RecyclerView不足一屏,则显示TextView,设置点击触发刷新。

要推断RecyclerView是否超过一屏,能够使用RecyclerView提供的computeVerticalScrollOffset()方法,它表示RecyclerView垂直方向偏移量。在载入底部刷新View时。推断其值是否为0。假设为0,表示RecyclerView不足一屏。假设返回值大于0。则表示超过一屏。

12. 支持水平方向的RecyclerView

对水平方向滑动的RecyclerView。上述底部刷新实现的原理和细节处理都是全然一样的,仅仅须要将底部刷新View的布局调整为竖直的,然后将一些和方向有关的接口更改一下就可以。比如mBottomViewVisibleDy须要改为mBottomViewVisibleDx,getHeight()改成getWidth()等。这里就不多介绍了。

13. 完整代码

最后将整个实现的完整代码贴在这里。只是这里底部刷新View仍然是ProgressBar,没有改造成ProgressBar和TextView的组合。也就是说。对“获取数据失败后的处理”。“所有数据获取完毕的处理”和“当前数据不足一屏的处理”都是採用的方法1。假设须要採用方法2的,能够自行实现。建议将底部刷新View採用自己定义View实现,将相关状态封装在自己定义View中。此外,这里仅仅支持垂直方向滑动的RecyclerView。对水平方向滑动的RecyclerView,因为没实用到。所以没有实现。

public class XRecyclerView extends RecyclerView {

    private OnBottomRefreshListener mBottomRefreshListener;
    private RecyclerView.OnScrollListener mOnScrollListener;
    private boolean mBottomRefreshing;
    private boolean mBottomRefreshable;
    private boolean mShouldHideAfterScrollIdle;

    public XRecyclerView(Context context) {
        super(context);
        init();
    }

    public XRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mBottomRefreshing = false;
        mBottomRefreshable = false;
        mShouldHideAfterScrollIdle = false;
        mBottomRefreshListener = null;

        mOnScrollListener = new RecyclerView.OnScrollListener() {

            // 此变量为true表示的意思是此轮滑动过程已经运行过一次bottom refresh了
            // 设想在一次滑动过程中,假设已经运行过一次bottom refresh,这时手指不离开屏幕,
            // 接着收到bottom refresh结果。将bottom refresh状态置为false,这时假设仍然在滑动,即使bottom view又变得可见了。也不应当再次运行bottom refresh
            private boolean mAlreadyRefreshed = false;
            private int mBottomViewVisibleDy = 0;
            private int mBottomViewHeight = 0;

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    boolean shouldHide = false;             // 是否须要隐藏bottom view
                    // 假设须要隐藏bottom view,则将shouldHide置为true
                    if (mShouldHideAfterScrollIdle) {
                        shouldHide = true;
                        mShouldHideAfterScrollIdle = false;
                    }
                    // 假设之前还没有滑到指定的位置就停止了滑动,则相同将shouldHide置为true
                    if (mBottomViewVisibleDy != 0) {
                        if (mBottomViewVisibleDy > 0) {
                            shouldHide = true;
                        }
                        mBottomViewVisibleDy = 0;
                    }
                    // 隐藏bottom view
                    if (shouldHide) {
                        hideBottomView();
                    }
                    if (mAlreadyRefreshed) {
                        mAlreadyRefreshed = false;
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                // 假设当前不可刷新,或者正在刷新,则不运行bottom refresh操作
                if (!isBottomRefreshable() || isBottomRefreshing() || mAlreadyRefreshed || dy == 0) {
                    return;
                }
                if (isBottomViewVisible()) {
                    // dy是本次调用onScrolled和上次调用onScrolled在y轴方向的偏移量。这里将bottom view可见之后的偏移量累加起来
                    mBottomViewVisibleDy += dy;
                    if (mBottomViewHeight == 0) {
                        View itemView = getLastVisibleItem();
                        if (itemView != null) {
                            mBottomViewHeight = itemView.getHeight();
                        }
                    }
                    // 假设bottom view可见之后的y轴偏移量大于bottom view高度的一半,则运行bottom refresh
                    if (mBottomViewHeight != 0 && mBottomViewVisibleDy > mBottomViewHeight / 2) {
                        if (mBottomRefreshListener != null) {
                            mBottomRefreshListener.onBottomRefresh();
                            mBottomRefreshing = true;
                            mAlreadyRefreshed = true;
                        }
                        mBottomViewVisibleDy = 0;
                    }
                } else {
                    mBottomViewVisibleDy = 0;
                }
            }
        };
    }

    public View getFirstVisibleItem() {
        return getLayoutManager().getChildAt(0);
    }

    public View getSecondVisibleItem() {
        return getLayoutManager().getChildAt(1);
    }

    private View getLastVisibleItem() {
        int firstItemPosition = getFirstVisibleItemPosition();
        int lastItemPosition = getLastVisibleItemPosition();
        if (firstItemPosition != NO_POSITION && lastItemPosition != NO_POSITION) {
            return getLayoutManager().getChildAt(lastItemPosition - firstItemPosition);
        } else {
            return null;
        }
    }

    private int getFirstVisibleItemPosition() {
        RecyclerView.LayoutManager manager = getLayoutManager();
        if (manager instanceof LinearLayoutManager) {
            return ((LinearLayoutManager) manager).findFirstVisibleItemPosition();
        } else if (manager instanceof StaggeredGridLayoutManager) {
            int positions[] = ((StaggeredGridLayoutManager) manager).findFirstVisibleItemPositions(null);
            int min = Integer.MAX_VALUE;
            for (int pos : positions) {
                if (pos < min) {
                    min = pos;
                }
            }
            return min;
        }
        return NO_POSITION;
    }

    private int getLastVisibleItemPosition() {
        RecyclerView.LayoutManager manager = getLayoutManager();
        if (manager instanceof LinearLayoutManager) {
            return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
        } else if (manager instanceof StaggeredGridLayoutManager) {
            int positions[] = ((StaggeredGridLayoutManager) manager).findLastVisibleItemPositions(null);
            int max = NO_POSITION;
            for (int pos : positions) {
                if (pos > max) {
                    max = pos;
                }
            }
            return max;
        }
        return NO_POSITION;
    }

    private boolean isBottomViewVisible() {
        int lastVisibleItem = getLastVisibleItemPosition();
        return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;
    }

    // 滑到顶部
    public void gotoTop() {
        smoothScrollToPosition(0);
    }

    // 设置为没有数据了
    public void setBottomRefreshable(boolean refreshable) {
        mBottomRefreshable = refreshable;
        getAdapter().notifyDataSetChanged();
    }

    public boolean isBottomRefreshable() {
        return mBottomRefreshable;
    }

    @Override
    public void setAdapter(RecyclerView.Adapter adapter) {
        if (adapter != null) {
            WrapperAdapter wrapperAdapter = new WrapperAdapter(adapter);
            super.setAdapter(wrapperAdapter);
        }
    }

    // 设置底部下拉刷新监听
    public void setOnBottomRefreshListener(OnBottomRefreshListener listener) {
        mBottomRefreshListener = listener;
        if (mBottomRefreshListener != null) {
            addOnScrollListener(mOnScrollListener);
            mBottomRefreshable = true;
        } else {
            removeOnScrollListener(mOnScrollListener);
            mBottomRefreshable = false;
        }
    }

    // 当前是否正在bottom refreshing
    public boolean isBottomRefreshing() {
        return mBottomRefreshing;
    }

    // 下拉刷新完毕之后须要隐藏bottom view
    public void onBottomRefreshComplete() {
        mBottomRefreshing = false;
        // 假设当前没有在滑动状态。则直接隐藏
        // 假设当前在滑动状态,则等待滑动停止后再隐藏
        if (getScrollState() == SCROLL_STATE_IDLE) {
            hideBottomView();
            mShouldHideAfterScrollIdle = false;
        } else {
            mShouldHideAfterScrollIdle = true;
        }
    }

    // 隐藏bottom view
    // 假设bottom view是可见的。则依据bottom view 当前的位置和RecyclerView当前位置来决定要向上滑动的距离
    private void hideBottomView() {
        if (isBottomViewVisible()) {
            View bottomView = getLastVisibleItem();
            if (bottomView != null) {
                int[] bottomViewLocation = new int[2];
                bottomView.getLocationInWindow(bottomViewLocation);
                int[] recyclerViewLocation = new int[2];
                getLocationInWindow(recyclerViewLocation);
                int recyclerViewHeight = getHeight();
                int offset = recyclerViewLocation[1] + recyclerViewHeight - bottomViewLocation[1];
                if (offset > 0) {
                    scrollBy(0, -offset);
                }
            }
        }
    }

    public interface OnBottomRefreshListener {
        void onBottomRefresh();
    }

    /**
     * 自己定义包裹的Adapter,主要用来处理载入很多其它视图
     */
    private class WrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;

        /**
         * 被包裹的外部Adapter
         */
        private RecyclerView.Adapter mInnerAdapter;

        private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
            @Override
            public void onChanged() {
                super.onChanged();
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeChanged(int positionStart, int itemCount) {
                super.onItemRangeChanged(positionStart, itemCount);
                notifyItemRangeChanged(positionStart, itemCount);
            }

            @Override
            public void onItemRangeInserted(int positionStart, int itemCount) {
                super.onItemRangeInserted(positionStart, itemCount);
                notifyItemRangeInserted(positionStart, itemCount);
            }

            @Override
            public void onItemRangeRemoved(int positionStart, int itemCount) {
                super.onItemRangeRemoved(positionStart, itemCount);
                notifyItemRangeRemoved(positionStart, itemCount);
            }

            @Override
            public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
                super.onItemRangeMoved(fromPosition, toPosition, itemCount);
                notifyItemRangeChanged(fromPosition, toPosition + itemCount);
            }
        };

        private WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {
            if (mInnerAdapter != null) {
                notifyItemRangeRemoved(0, mInnerAdapter.getItemCount());
                mInnerAdapter.unregisterAdapterDataObserver(dataObserver);
            }
            this.mInnerAdapter = adapter;
            mInnerAdapter.registerAdapterDataObserver(dataObserver);
            notifyItemRangeInserted(0, mInnerAdapter.getItemCount());
        }

        public boolean isLoadMoreView(int position) {
            return isBottomRefreshable() && position == getItemCount() - 1;
        }

        @Override
        public int getItemCount() {
            if (mInnerAdapter != null) {
                int itemCount = mInnerAdapter.getItemCount();
                if (isBottomRefreshable()) {
                    return itemCount + 1;
                } else {
                    return itemCount;
                }
            } else {
                return 0;
            }
        }

        @Override
        public int getItemViewType(int position) {
            if (isBottomRefreshable()) {
                if (mInnerAdapter != null) {
                    int adapterCount = mInnerAdapter.getItemCount();
                    if (position < adapterCount) {
                        return mInnerAdapter.getItemViewType(position);
                    }
                }
                return TYPE_BOTTOM_REFRESH_ITEM;
            } else {
                if (mInnerAdapter != null) {
                    return mInnerAdapter.getItemViewType(position);
                } else {
                    return super.getItemViewType(position);
                }
            }
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {
                View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);
                return new BottomRefreshViewHolder(view);
            } else {
                return mInnerAdapter.onCreateViewHolder(parent, viewType);
            }
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (!(holder instanceof BottomRefreshViewHolder)) {
                if (mInnerAdapter != null) {
                    int adapterCount = mInnerAdapter.getItemCount();
                    if (position <adapterCount) {
                        mInnerAdapter.onBindViewHolder(holder, position);
                    }
                }
            }
        }

        @Override
        public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
            super.onViewAttachedToWindow(holder);
            if (holder instanceof BottomRefreshViewHolder) {
                // 支持瀑布流布局
                ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
                if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                    ((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);
                }
            }
        }

        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            // 对Grid布局进行支持
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager instanceof GridLayoutManager) {
                final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
                final GridLayoutManager.SpanSizeLookup lookup = gridLayoutManager.getSpanSizeLookup();
                gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                    @Override
                    public int getSpanSize(int position) {
                        return isLoadMoreView(position) ? gridLayoutManager.getSpanCount() : lookup.getSpanSize(position) ;
                    }
                });
            }
        }
    }

    /**
     * bottom refresh View相应的ViewHolder
     */
    private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {
        BottomRefreshViewHolder(View itemView) {
            super(itemView);
        }
    }
}
时间: 2024-10-10 11:00:04

RecyclerView底部刷新实现具体解释的相关文章

Android RecyclerView局部刷新那个坑

关键:public final void notifyItemChanged(int position, Object payload) RecyclerView局部刷新大家都遇到过,有时候还说会遇见图片闪烁的问题. 优化之前的效果: 优化之后的效果: 如果想单独更新一个item,我们通常会这样做,代码如下: mLRecyclerViewAdapter.notifyItemChanged(position); 这里的position就是那个列表项的索引,调用这个方法可以更新一个Item的UI(当

再说Android RecyclerView局部刷新那个坑

关键:public final void notifyItemChanged(int position, Object payload) RecyclerView局部刷新大家都遇到过,有时候还说会遇见图片闪烁的问题. 优化之前的效果: 优化之后的效果: 如果想单独更新一个item,我们通常会这样做,代码如下: mLRecyclerViewAdapter.notifyItemChanged(position); 1 这里的position就是那个列表项的索引,调用这个方法可以更新一个Item的UI

安卓易学,爬坑不易——腾讯老司机的RecyclerView局部刷新爬坑之路

针对手游的性能优化,腾讯WeTest平台的Cube工具提供了基本所有相关指标的检测,为手游进行最高效和准确的测试服务,不断改善玩家的体验.目前功能还在免费开放中. 点击地址:http://wetest.qq.com/cube立即体验! 作者:Hoolly,腾讯移动客户端开发工程师. 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处 WeTest导读 安卓开发者都知道,RecyclerView比ListView要灵活的多,但不可否认的里面的坑也同样埋了不少人.下面让我们看看腾讯开发工程

页面回滚效果(滚动条到底部刷新页面)

jquery滚动监听方法如下, $(window).scroll(function(){ var scrollTop = $(this).scrollTop(); var scrollHeight = $(document).height(); var windowHeight = $(this).height(); if(scrollTop + windowHeight == scrollHeight){ alert("you are in the bottom"); } }); 在

RecyclerView 数据刷新的几种方式

小结 刷新全部可见的item,notifyDataSetChanged() 刷新指定item,notifyItemChanged(int) 从指定位置开始刷新指定个item,notifyItemRangeChanged(int,int) 插入.移动一个并自动刷新,notifyItemInserted(int).notifyItemMoved(int).notifyItemRemoved(int) 局部刷新,notifyItemChanged(int, Object) Activity publi

android:RecyclerView局部刷新那点事~

1.局部刷新的引入提到RecyclerView,我们首先想到的是ListView,对于ListView的局部刷新,我们之前已经有解决方案,[android:ListView的局部刷新]当时的解决方案是:记录点击的Item的position,然后在更新过程中,不断的判断,该position是不是介于可见的Item之间,如果是,则更新,否者,不更新.2.RecyclerView的局部更新 按照之前的思路,首先要寻找RecyclerView中可见的item的位置范围,该方法并不在RecyclerVie

手机页面滚动到底部刷新数据

1 //滚动到底部加载数据 2 $(window).scroll(function () { 3 var scrollTop = $(this).scrollTop(); 4 var elementHeight = $(this).height(); 5 var h1 = scrollTop + elementHeight; 6 var scrollHeight = document.documentElement.scrollHeight; //var documentHeight = $(d

JQuery - 点击,滚动回到顶部 / 底部刷新回到顶部

if ($(document).scrollTop() != 0) { //刷新之后,回到顶部 $('body,html').animate({ scrollTop: 0 }, 500); }

RecyclerView的万能适配器+定义可以到底部自动刷新的RecyclerView

RecyclerView的重要性就不做重复说明了,为了方便以后直接使用写了这个,主要有: 万能适配器在使用的时候分为定义布局和绑定数据,方便直接套用.加入了底部刷新,需要配合自己写的RecyclerView一起使用,对于布局中各个子布局和控件可以做到响应各种点击事件: 首先抽取ViewHolder:这里的要点是用到了SparseArray(源码中类似ArrayList.直接使用Object数组进行实现): package com.fightzhao.baseadapterdemo.base; i