android 增强版 RecyclerView

  1. 多 Header / Footer 支持

    • 原来只能添加一个,现在可以通过 addHeaderView() / addFooterView() 多次添加。
  2. 空数据视图

    • 通过 setEmptyView() 设置,当列表无数据时自动显示。
  3. 下拉刷新

    • 自定义头部视图(默认提供一个简单样式)。
    • 可通过 setPullRefreshListener() 设置刷新回调。
  4. 上拉加载更多

    • 自定义底部视图(默认提供一个简单样式)。
    • 可通过 setLoadMoreListener() 设置加载更多回调。
  5. Item 点击 / 长按事件

    • 简化外部监听,直接返回业务数据的真实位置。
  6. 滚动监听

    • 提供更友好的滚动回调接口。
  7. 兼容性

    • 兼容 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 ,并且全部代码都有详细的中文注释。

这样做的好处

✅ 功能解耦 :刷新和加载更多逻辑独立,方便替换不同实现✅ 灵活扩展 :支持自带实现和第三方库✅ 控制方便 :一行代码开关功能✅ 易于维护:接口清晰,逻辑分明

相关推荐
千里马学框架6 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台6 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone6 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc6 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo6 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077006 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼6 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone6 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen6 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone6 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui