-
多 Header / Footer 支持
- 原来只能添加一个,现在可以通过
addHeaderView()
/addFooterView()
多次添加。
- 原来只能添加一个,现在可以通过
-
空数据视图
- 通过
setEmptyView()
设置,当列表无数据时自动显示。
- 通过
-
下拉刷新
- 自定义头部视图(默认提供一个简单样式)。
- 可通过
setPullRefreshListener()
设置刷新回调。
-
上拉加载更多
- 自定义底部视图(默认提供一个简单样式)。
- 可通过
setLoadMoreListener()
设置加载更多回调。
-
Item 点击 / 长按事件
- 简化外部监听,直接返回业务数据的真实位置。
-
滚动监听
- 提供更友好的滚动回调接口。
-
兼容性
- 兼容 Android 4.1 (API 16)。
8、刷新和加载
-
是否启用下拉刷新
-
是否启用上拉加载
-
使用自带的刷新 / 加载更多功能 还是 接入第三方库(如 SmartRefreshLayout、TwinklingRefreshLayout)
-
下拉刷新
- 在
DefaultPullRefreshImpl
中实现触摸事件处理 - 添加下拉弹性动画和平滑回弹效果
- 状态管理(下拉中、释放可刷新、刷新中、完成)
- 在
-
上拉加载
- 在
DefaultLoadMoreImpl
中实现滚动监听 - 当滚动到底部时触发加载更多
- 加载完成后更新状态
- 在
设计思路
- 接口隔离
- 定义
PullRefreshable
和LoadMoreable
接口,里面只定义刷新和加载的方法。
- 定义
- 默认实现
- 写一个
DefaultPullRefreshImpl
和DefaultLoadMoreImpl
实现自带的下拉刷新和上拉加载。
- 写一个
- 第三方实现
- 如果你想用第三方库,只需要实现对应的接口,注入到
XCRecyclerView
中。
- 如果你想用第三方库,只需要实现对应的接口,注入到
- 开关控制
- 在
XCRecyclerView
中提供setPullRefreshEnabled(boolean)
和setLoadMoreEnabled(boolean)
方法。
- 在
代码如下
1、接口定义
LoadMoreable
package com.nyw.mvvmmode.widget.recyclerview;
/**
* 上拉加载更多功能接口
* 定义上拉加载的核心操作:启用/禁用、设置监听、触发加载、判断加载状态、设置是否有更多数据
*/
public interface LoadMoreable {
/**
* 启用或禁用上拉加载更多功能
* @param enable true=启用,false=禁用
*/
void setLoadMoreEnabled(boolean enable);
/**
* 设置上拉加载的回调监听(触发加载时会回调此接口)
* @param listener 加载监听器,传null表示取消监听
*/
void setOnLoadMoreListener(LoadMoreListener listener);
/**
* 主动触发或停止上拉加载
* @param loading true=触发加载(显示加载动画),false=停止加载(隐藏动画)
*/
void setLoading(boolean loading);
/**
* 判断当前是否处于上拉加载状态
* @return true=正在加载,false=未加载
*/
boolean isLoading();
/**
* 设置是否还有更多数据可加载
* @param hasMore true=有更多数据(触发加载时会回调),false=无更多数据(隐藏加载视图)
*/
void setHasMore(boolean hasMore);
}
LoadMoreListener
package com.nyw.mvvmmode.widget.recyclerview;
/**
* 上拉加载更多的回调接口
* 当用户上拉到底部触发加载,或通过代码主动触发加载时,会回调此接口的onLoadMore方法
*/
public interface LoadMoreListener {
/**
* 加载触发时的回调方法
* 在这里处理加载更多逻辑(如请求下一页数据、添加到列表)
*/
void onLoadMore();
}
PullRefreshable
package com.nyw.mvvmmode.widget.recyclerview;
/**
* 下拉刷新功能接口
* 定义下拉刷新的核心操作:启用/禁用、设置监听、触发刷新、判断刷新状态
*/
public interface PullRefreshable {
/**
* 启用或禁用下拉刷新功能
* @param enable true=启用,false=禁用
*/
void setPullRefreshEnabled(boolean enable);
/**
* 设置下拉刷新的回调监听(刷新触发时会回调此接口)
* @param listener 刷新监听器,传null表示取消监听
*/
void setOnRefreshListener(PullRefreshListener listener);
/**
* 主动触发或停止下拉刷新
* @param refreshing true=触发刷新(显示刷新动画),false=停止刷新(隐藏动画)
*/
void setRefreshing(boolean refreshing);
/**
* 判断当前是否处于下拉刷新状态
* @return true=正在刷新,false=未刷新
*/
boolean isRefreshing();
}
PullRefreshListener
package com.nyw.mvvmmode.widget.recyclerview;
/**
* 下拉刷新的回调接口
* 当用户下拉触发刷新,或通过代码主动触发刷新时,会回调此接口的onRefresh方法
*/
public interface PullRefreshListener {
/**
* 刷新触发时的回调方法
* 在这里处理刷新逻辑(如请求网络数据、更新列表)
*/
void onRefresh();
}
2️⃣ 默认下拉刷新实现(带动画)
package com.nyw.mvvmmode.widget;
import android.view.*;
import android.view.animation.*;
import android.widget.*;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
public class DefaultPullRefreshImpl implements PullRefreshable {
private final XCRecyclerView recyclerView;
private final View refreshHeader;
private final ProgressBar progressBar;
private final TextView statusText;
private boolean enabled = true;
private PullRefreshListener listener;
private boolean refreshing = false;
private int headerHeight;
private float downY;
private boolean isDragging = false;
private int refreshState = STATE_IDLE;
private static final int STATE_IDLE = 0; // 空闲
private static final int STATE_PULLING = 1; // 下拉中
private static final int STATE_RELEASING = 2; // 释放可刷新
private static final int STATE_REFRESHING = 3;// 刷新中
public DefaultPullRefreshImpl(XCRecyclerView recyclerView) {
this.recyclerView = recyclerView;
// 加载刷新头部布局
this.refreshHeader = LayoutInflater.from(recyclerView.getContext())
.inflate(R.layout.xc_recyclerview_refresh_header, null);
this.progressBar = refreshHeader.findViewById(R.id.refresh_progress);
this.statusText = refreshHeader.findViewById(R.id.refresh_status_text);
// 测量头部高度
measureHeaderHeight();
// 默认隐藏头部
setHeaderTopMargin(-headerHeight);
// 添加到RecyclerView的Header
recyclerView.addHeaderView(refreshHeader);
// 设置触摸监听
setupTouchListener();
}
/**
* 测量Header高度
*/
private void measureHeaderHeight() {
int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
refreshHeader.measure(w, h);
headerHeight = refreshHeader.getMeasuredHeight();
}
/**
* 设置Header的marginTop
*/
private void setHeaderTopMargin(int margin) {
ViewGroup.LayoutParams lp = refreshHeader.getLayoutParams();
if (lp instanceof ViewGroup.MarginLayoutParams) {
((ViewGroup.MarginLayoutParams) lp).topMargin = margin;
refreshHeader.setLayoutParams(lp);
}
}
/**
* 设置触摸监听,处理下拉刷新逻辑
*/
private void setupTouchListener() {
recyclerView.setOnTouchListener((v, event) -> {
if (!enabled || refreshing) return false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = event.getY();
isDragging = false;
break;
case MotionEvent.ACTION_MOVE:
float moveY = event.getY();
float diff = moveY - downY;
// 仅在顶部且向下拉时触发
if (diff > 0 && !recyclerView.canScrollVertically(-1)) {
isDragging = true;
float offset = diff / 2; // 阻尼效果
setHeaderTopMargin((int) (-headerHeight + offset));
// 更新状态文字
if (offset >= headerHeight) {
refreshState = STATE_RELEASING;
statusText.setText("释放刷新");
} else {
refreshState = STATE_PULLING;
statusText.setText("下拉刷新");
}
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (isDragging) {
if (refreshState == STATE_RELEASING) {
startRefresh(); // 进入刷新
} else {
resetHeader(); // 回弹
}
isDragging = false;
downY = -1;
return true;
}
break;
}
return false;
});
}
/**
* 开始刷新
*/
private void startRefresh() {
refreshing = true;
refreshState = STATE_REFRESHING;
statusText.setText("正在刷新...");
progressBar.setVisibility(View.VISIBLE);
// 平滑显示Header
ValueAnimator animator = ValueAnimator.ofInt(refreshHeader.getTop(), 0);
animator.setDuration(300);
animator.setInterpolator(new FastOutSlowInInterpolator());
animator.addUpdateListener(animation ->
setHeaderTopMargin((Integer) animation.getAnimatedValue())
);
animator.start();
if (listener != null) {
listener.onRefresh();
}
}
/**
* 重置Header
*/
private void resetHeader() {
ValueAnimator animator = ValueAnimator.ofInt(refreshHeader.getTop(), -headerHeight);
animator.setDuration(300);
animator.setInterpolator(new FastOutSlowInInterpolator());
animator.addUpdateListener(animation ->
setHeaderTopMargin((Integer) animation.getAnimatedValue())
);
animator.start();
refreshState = STATE_IDLE;
statusText.setText("下拉刷新");
progressBar.setVisibility(View.GONE);
}
@Override
public void setPullRefreshEnabled(boolean enable) {
this.enabled = enable;
}
@Override
public void setOnRefreshListener(PullRefreshListener listener) {
this.listener = listener;
}
@Override
public void setRefreshing(boolean refreshing) {
if (this.refreshing == refreshing) return;
this.refreshing = refreshing;
if (refreshing) {
startRefresh();
} else {
finishRefresh();
}
}
/**
* 结束刷新
*/
public void finishRefresh() {
refreshing = false;
refreshState = STATE_IDLE;
statusText.setText("下拉刷新");
progressBar.setVisibility(View.GONE);
resetHeader();
}
@Override
public boolean isRefreshing() {
return refreshing;
}
}
3️⃣ 默认上拉加载实现(带动画)
package com.nyw.mvvmmode.widget;
import android.view.*;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
public class DefaultLoadMoreImpl implements LoadMoreable {
private final XCRecyclerView recyclerView;
private final View loadMoreView;
private boolean enabled = true;
private LoadMoreListener listener;
private boolean loading = false;
private boolean hasMore = true;
public DefaultLoadMoreImpl(XCRecyclerView recyclerView) {
this.recyclerView = recyclerView;
// 加载加载更多布局
this.loadMoreView = LayoutInflater.from(recyclerView.getContext())
.inflate(R.layout.xc_recyclerview_load_more, null);
// 添加到底部
recyclerView.addFooterView(loadMoreView);
// 默认隐藏
loadMoreView.setVisibility(View.GONE);
// 设置滚动监听
setupScrollListener();
}
/**
* 设置滚动监听,判断是否到底部
*/
private void setupScrollListener() {
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (!enabled || loading || !hasMore) return;
// 滚动到底部且处于空闲状态时触发加载更多
if (newState == RecyclerView.SCROLL_STATE_IDLE &&
!recyclerView.canScrollVertically(1)) {
startLoadMore();
}
}
});
}
/**
* 开始加载更多
*/
private void startLoadMore() {
loading = true;
loadMoreView.setVisibility(View.VISIBLE);
// 淡入动画
AlphaAnimation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setDuration(300);
loadMoreView.startAnimation(fadeIn);
if (listener != null) {
listener.onLoadMore();
}
}
@Override
public void setLoadMoreEnabled(boolean enable) {
this.enabled = enable;
}
@Override
public void setOnLoadMoreListener(LoadMoreListener listener) {
this.listener = listener;
}
@Override
public void setLoading(boolean loading) {
this.loading = loading;
if (loading) {
startLoadMore();
} else {
finishLoadMore();
}
}
/**
* 结束加载更多
*/
public void finishLoadMore() {
loading = false;
// 淡出动画
AlphaAnimation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setDuration(300);
fadeOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
loadMoreView.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
loadMoreView.startAnimation(fadeOut);
}
@Override
public boolean isLoading() {
return loading;
}
@Override
public void setHasMore(boolean hasMore) {
this.hasMore = hasMore;
if (!hasMore) {
loadMoreView.setVisibility(View.GONE);
}
}
}
4️⃣ 刷新头部布局 res/layout/xc_recyclerview_refresh_header.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="12dp">
<ProgressBar
android:id="@+id/refresh_progress"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminate="true"
android:visibility="gone" />
<TextView
android:id="@+id/refresh_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="下拉刷新"
android:textColor="#666666"
android:textSize="14sp" />
</LinearLayout>
5️⃣ 加载更多布局 res/layout/xc_recyclerview_load_more.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/load_more_progress"
android:layout_width="20dp"
android:layout_height="20dp"
android:indeterminate="true" />
<TextView
android:id="@+id/load_more_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="正在加载更多..."
android:textColor="#666666"
android:textSize="14sp" />
</LinearLayout>
6️⃣ 核心 XCRecyclerView(支持切换刷新 / 加载实现)
package com.nyw.mvvmmode.widget;
import android.content.Context;
import android.util.AttributeSet;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class XCRecyclerView extends RecyclerView {
private PullRefreshable pullRefreshImpl;
private LoadMoreable loadMoreImpl;
// Header & Footer
private final List<View> mHeaderViews = new ArrayList<>();
private final List<View> mFooterViews = new ArrayList<>();
public XCRecyclerView(Context context) {
super(context);
init(context);
}
public XCRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public XCRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
// 默认使用自带的下拉刷新和上拉加载
pullRefreshImpl = new DefaultPullRefreshImpl(this);
loadMoreImpl = new DefaultLoadMoreImpl(this);
}
/**
* 添加Header
*/
public void addHeaderView(View view) {
mHeaderViews.add(view);
}
/**
* 添加Footer
*/
public void addFooterView(View view) {
mFooterViews.add(view);
}
/**
* 切换下拉刷新实现(可传入第三方)
*/
public void setPullRefreshImpl(PullRefreshable impl) {
this.pullRefreshImpl = impl;
}
/**
* 切换上拉加载实现(可传入第三方)
*/
public void setLoadMoreImpl(LoadMoreable impl) {
this.loadMoreImpl = impl;
}
/**
* 启用/禁用下拉刷新
*/
public void setPullRefreshEnabled(boolean enable) {
pullRefreshImpl.setPullRefreshEnabled(enable);
}
/**
* 启用/禁用上拉加载
*/
public void setLoadMoreEnabled(boolean enable) {
loadMoreImpl.setLoadMoreEnabled(enable);
}
/**
* 设置下拉刷新监听
*/
public void setOnRefreshListener(PullRefreshListener listener) {
pullRefreshImpl.setOnRefreshListener(listener);
}
/**
* 设置上拉加载监听
*/
public void setOnLoadMoreListener(LoadMoreListener listener) {
loadMoreImpl.setOnLoadMoreListener(listener);
}
/**
* 主动触发刷新
*/
public void setRefreshing(boolean refreshing) {
pullRefreshImpl.setRefreshing(refreshing);
}
/**
* 主动触发加载更多
*/
public void setLoading(boolean loading) {
loadMoreImpl.setLoading(loading);
}
/**
* 设置是否还有更多数据
*/
public void setHasMore(boolean hasMore) {
loadMoreImpl.setHasMore(hasMore);
}
}
7️⃣ 使用示例
XCRecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 启用下拉刷新
recyclerView.setPullRefreshEnabled(true);
recyclerView.setOnRefreshListener(() -> {
new Handler().postDelayed(() -> {
// 刷新数据
recyclerView.setRefreshing(false);
}, 2000);
});
// 启用上拉加载
recyclerView.setLoadMoreEnabled(true);
recyclerView.setOnLoadMoreListener(() -> {
new Handler().postDelayed(() -> {
// 加载数据
recyclerView.setLoading(false);
}, 2000);
});
recyclerView.setAdapter(adapter);
一个完整的、带动画的、可切换第三方库的 XCRecyclerView
,并且全部代码都有详细的中文注释。
这样做的好处
✅ 功能解耦 :刷新和加载更多逻辑独立,方便替换不同实现✅ 灵活扩展 :支持自带实现和第三方库✅ 控制方便 :一行代码开关功能✅ 易于维护:接口清晰,逻辑分明