简述
该增强型RecyclerView,增加了以下特性:
- 上拉滑动到底部,加载更多
- 支持添加Header头视图
- 支持加载数据为空时,显示特定视图
- 支持拖拽,侧滑删除
下拉刷新实现通过给RecyclerView包一层SwipRefreshLayout来实现。
本文重点分享上拉加载更多的实现,同时实现添加头部视图,侧滑,拖拽功能实现,该实现存在以下注意点:
- 如何判断RecyclerView滑动到了底部
- 通常RecyclerView显示的item布局相同,怎么做到上拉加载更多时出现一个底栏视图
- 滑动到底栏出现加载更多动画,这个动画什么时候结束?动画生命周期是?
- ReclcerViewyou多种布局,如果是网格布局(有多列),怎么做到让上拉加载更多的动画视图和头部视图占用一整行?
- 自定义了RecyclerView,如何做到像使用标准RecyclerView那样使用?
- 如何实现item的拖拽和侧滑删除?
实现的注意点解析
如何判断RecyclerView滑动到了底部
关于布局的逻辑设置,就找LayoutManager。的确,通过查阅官方API手册,有findLastVisibleItemPosition()/findLastCompletelyVisibleItePosition(),在滑动监听里,使用这两个方法就能实现判断
怎么做到上拉加载更多时出现一个底栏视图
这里涉及到RecyclerView如何实现多布局显示的知识,通过getItemViewType(),底部上拉加载更多视图设定一种ItemViewType值,对应新建一个ViewHolder.
滑动到底栏出现加载更多动画,这个动画什么时候结束?动画生命周期是?
动画的开始时刻是列表滑倒底部,当滑倒底部时,在客户类(Fragment/Activity)里接口回调,开始网络请求数据,动画的结束时刻是网络加载完成,刷新列表时
ReclcerViewyou多种布局,如果是网格布局(有多列),怎么做到让上拉加载更多的动画视图和头部视图占用一整行?
和布局相关的,找LayoutManager,这里要找GridLayoutManager,它提供了setSpanSizeLookup(GridLayoutManager.SpanSizeLookup),通过这个方法,可以根据位置来设置item占用一整行还是正常显示
自定义了RecyclerView,如何做到像使用标准RecyclerView那样使用?
使用装饰器设计模式,能很好的实现对用户透明使用效果
如何实现item的拖拽和侧滑删除?
使用android提供的ItemTouchHelper工具类,能快速的实现
核心代码
EnhanceRecyclerView
public class EnhanceRecyclerView extends RecyclerView {
private static final String TAG = "EnhanceRecyclerView";
private OnLoadMoreListener mOnLoadMoreListener;
private InternalAdapter mInternalAdapter;
private View mEmptyView;
private @LayoutRes int mHeaderResId;
private AdapterDataObserver mAdapterDataObserver = new EnhanceAdapterDataObserver();
/**
* 滚动方向
*/
private int mScrollDy = 0;
public EnhanceRecyclerView(Context context) {
super(context);
}
public EnhanceRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public EnhanceRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onScrolled(int dx, int dy) {
super.onScrolled(dx, dy);
mScrollDy = dy;
}
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
switch (state) {
case SCROLL_STATE_IDLE:
LayoutManager layoutManager = getLayoutManager();
int itemCount = getAdapter().getItemCount();
int lastVisibleItemPosition = 0;
if (layoutManager instanceof GridLayoutManager) {
GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
lastVisibleItemPosition = gridLayoutManager.findLastVisibleItemPosition();
} else if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
}
if (lastVisibleItemPosition >= itemCount - 1) {
if (getParent() instanceof SwipeRefreshLayout) {
SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getParent();
if (swipeRefreshLayout.isRefreshing()) {
break;
}
}
if (mOnLoadMoreListener != null && mScrollDy > 0) {
mInternalAdapter.setLoadingIndicatorViewVisible(VISIBLE);
mOnLoadMoreListener.onLoadMore();
}
}
break;
}
}
/**
* 重写此方法,设置GridLayout的上拉加载更多视图的位置
*
* @param layout
*/
@Override
public void setLayoutManager(LayoutManager layout) {
if (layout instanceof GridLayoutManager) {
final GridLayoutManager externalGridLayoutManager = (GridLayoutManager) layout;
final int spanCount = externalGridLayoutManager.getSpanCount();
int orientation = externalGridLayoutManager.getOrientation();
final GridLayoutManager innerGridLayoutManager = new GridLayoutManager(getContext(), spanCount, orientation, false);
innerGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int headerViewCount = mInternalAdapter.getHeaderViewCount();
int footViewCount = mInternalAdapter.getFootViewCount();
if (position < headerViewCount) {
return spanCount;
}
int totalItemCount = innerGridLayoutManager.getItemCount();
if (position >= totalItemCount - footViewCount) {
return spanCount;
}
return externalGridLayoutManager.getSpanSizeLookup().getSpanSize(position - headerViewCount);
}
});
super.setLayoutManager(innerGridLayoutManager);
} else {
super.setLayoutManager(layout);
}
}
public View getEmptyView() {
return mEmptyView;
}
public final void setEmptyView(View emptyView) {
mEmptyView = emptyView;
setupEmptyViewHierarchy(emptyView);
}
protected void setupEmptyViewHierarchy(View emptyView) {
((ViewGroup) getParent().getParent()).addView(emptyView,0);
}
public void addHeaderResId(@LayoutRes int resId) {
mHeaderResId = resId;
if (mInternalAdapter != null) {
mInternalAdapter.setExternalHeaderResId(resId);
}
}
@Override
public void setAdapter(Adapter adapter) {
mInternalAdapter = new InternalAdapter(adapter);
super.setAdapter(mInternalAdapter);
//addHeaderView方法依赖于setAdapter方法
if (mHeaderResId > 0) {
addHeaderResId(mHeaderResId);
}
mInternalAdapter.registerAdapterDataObserver(mAdapterDataObserver);
mAdapterDataObserver.onChanged();
}
public void setOnLoadMoreListener(OnLoadMoreListener onLoadMoreListener) {
mOnLoadMoreListener = onLoadMoreListener;
}
public void loadMoreOnSuccess() {
if (mInternalAdapter != null) {
mInternalAdapter.loadMoreOnSuccess();
}
}
public void loadMoreOnError() {
if (mInternalAdapter != null) {
mInternalAdapter.loadMoreOnError();
}
}
public void loadMoreOnComplete() {
if (mInternalAdapter != null) {
mInternalAdapter.loadMoreOnComplete();
}
}
public final void notifyDataSetChanged() {
mInternalAdapter.notifyDataSetChanged();
}
public final void notifyItemChanged(int position) {
mInternalAdapter.notifyItemChanged(position);
}
public final void notifyItemChanged(int position, Object payload) {
position = position + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemChanged(position, payload);
}
public final void notifyItemRangeChanged(int positionStart, int itemCount) {
positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemRangeChanged(positionStart, itemCount);
}
public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemRangeChanged(positionStart, itemCount, payload);
}
public final void notifyItemInserted(int position) {
position = position + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemInserted(position);
}
public final void notifyItemMoved(int fromPosition, int toPosition) {
fromPosition = fromPosition + mInternalAdapter.getHeaderViewCount();
toPosition = toPosition + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemMoved(fromPosition, toPosition);
}
public final void notifyItemRangeInserted(int positionStart, int itemCount) {
positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemRangeInserted(positionStart, itemCount);
}
public final void notifyItemRemoved(int position) {
position = position + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemRemoved(position);
}
public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
mInternalAdapter.notifyItemRangeRemoved(positionStart, itemCount);
}
public InternalAdapter getInternalAdapter() {
return mInternalAdapter;
}
/**
* 上拉加载更多回调
*/
public interface OnLoadMoreListener {
void onLoadMore();
}
private class EnhanceAdapterDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
super.onChanged();
if (getEmptyView() != null && getAdapter() != null) {
int itemCount = getAdapter().getItemCount();
if (itemCount == 0) {
getEmptyView().setVisibility(VISIBLE);
setVisibility(GONE);
} else {
getEmptyView().setVisibility(GONE);
setVisibility(VISIBLE);
}
}
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
onChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
super.onItemRangeChanged(positionStart, itemCount, payload);
onChanged();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
onChanged();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
onChanged();
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
onChanged();
}
}
}
InternalAdapter
public class InternalAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = "InternalAdapter";
private static final int HEADER_ITEM_TYPE = 170118;
private static final int FOOTER_ITEM_TYPE = 170116;
private RecyclerView.Adapter<RecyclerView.ViewHolder> mExternalAdapter;
private int mBodyItemCount;
private FooterView mFooterView;
private @LayoutRes int mExternalHeaderResId;
public InternalAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> externalAdapter) {
mExternalAdapter = externalAdapter;
mBodyItemCount = externalAdapter.getItemCount();
}
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return HEADER_ITEM_TYPE;
}
else if (isFootView(position)) {
return FOOTER_ITEM_TYPE;
}
return mExternalAdapter.getItemViewType(position);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case HEADER_ITEM_TYPE:
View headerView = LayoutInflater.from(parent.getContext()).inflate(mExternalHeaderResId, parent, false);
return new HeaderView(headerView);
case FOOTER_ITEM_TYPE:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_footer_indicator, parent, false);
mFooterView = new FooterView(view);
return mFooterView;
default:
return mExternalAdapter.onCreateViewHolder(parent, viewType);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(isHeaderView(position)){
return;
}
if (isFootView(position)) {
return;
}
if(mExternalHeaderResId > 0){
position = position - getHeaderViewCount();
}
mExternalAdapter.onBindViewHolder(holder, position);
}
@Override
public int getItemCount() {
mBodyItemCount = mExternalAdapter.getItemCount();
if(mBodyItemCount == 0){
return 0;
}
else{
return getHeaderViewCount() + mBodyItemCount + getFootViewCount();
}
}
private boolean isHeaderView(int position){
return mExternalHeaderResId > 0 && position == 0;
}
private boolean isFootView(int position) {
return (position >= mBodyItemCount + getHeaderViewCount());
}
public int getFootViewCount() {
return 1;
}
public int getHeaderViewCount(){
return mExternalHeaderResId > 0 ? 1 : 0;
}
public void setLoadingIndicatorViewVisible(int visible){
if(mFooterView != null){
mFooterView.setLoadingIndicatorViewVisible(visible);
}
}
public void setExternalHeaderResId(int externalHeaderResId) {
mExternalHeaderResId = externalHeaderResId;
}
public void loadMoreOnSuccess(){
setLoadingIndicatorViewVisible(View.GONE);
}
public void loadMoreOnError(){
setLoadingIndicatorViewVisible(View.GONE);
}
public void loadMoreOnComplete(){
setLoadingIndicatorViewVisible(View.GONE);
}
static class HeaderView extends RecyclerView.ViewHolder{
HeaderView(View itemView) {
super(itemView);
}
}
static class FooterView extends RecyclerView.ViewHolder {
@Bind(R.id.item_footer_indicator)
LoadingIndicatorView mLoadingIndicatorView;
FooterView(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
mLoadingIndicatorView.setVisibility(View.GONE);
}
void setLoadingIndicatorViewVisible(int visible){
mLoadingIndicatorView.setVisibility(visible);
}
}
}
底部FooterView的布局item_footer_indicator.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/transparent"
>
<com.sugary.refreshrecyclerview.enhancerecycler.indicator.LoadingIndicatorView
android:id="@+id/item_footer_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:indicator_color="@color/indicator_loading_more_orange"
/>
</RelativeLayout>
LoadingIndicatorView
public class LoadingIndicatorView extends View {
//Sizes (with defaults in DP)
public static final int DEFAULT_SIZE = 50;
private Paint mPaint;
private BaseIndicatorController mIndicatorController;
private boolean mHasAnimation;
public LoadingIndicatorView(Context context) {
this(context, null);
}
public LoadingIndicatorView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.LoadingIndicatorView);
int indicatorColor = a.getColor(R.styleable.LoadingIndicatorView_indicator_color, Color.GRAY);
a.recycle();
mPaint = new Paint();
mPaint.setColor(indicatorColor);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mIndicatorController = new BallPulseIndicator();
mIndicatorController.setTarget(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = measureDimension(dp2px(DEFAULT_SIZE), widthMeasureSpec);
int height = measureDimension(dp2px(DEFAULT_SIZE), heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int measureDimension(int defaultSize, int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(defaultSize, specSize);
} else {
result = defaultSize;
}
return result;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!mHasAnimation) {
mHasAnimation = true;
mIndicatorController.initAnimation();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawIndicator(canvas);
}
void drawIndicator(Canvas canvas) {
mIndicatorController.draw(canvas, mPaint);
}
private int dp2px(int dpValue) {
return (int) getContext().getResources().getDisplayMetrics().density * dpValue;
}
@Override
public void setVisibility(int v) {
if (getVisibility() != v) {
super.setVisibility(v);
if (v == GONE || v == INVISIBLE) {
mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.END);
} else {
mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START);
}
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mHasAnimation) {
mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.CANCEL);
}
}
小结:
列表数据刷新,改成了调用EnhanceRecylerView方法,用自己建的Adapter刷新数据无效(这是这个轮子的缺陷,有待改进)。
底部滑动动画使用了他人的开源动画
在自制增强型RecyclerView过程中,也刷了一些资料,推荐阅读。
参考资料
RecyclerView必知必会(五星推荐)
网友评论