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 中实现滚动监听
    • 当滚动到底部时触发加载更多
    • 加载完成后更新状态

设计思路

  • 接口隔离
    • 定义 PullRefreshableLoadMoreable 接口,里面只定义刷新和加载的方法。
  • 默认实现
    • 写一个 DefaultPullRefreshImplDefaultLoadMoreImpl 实现自带的下拉刷新和上拉加载。
  • 第三方实现
    • 如果你想用第三方库,只需要实现对应的接口,注入到 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 ,并且全部代码都有详细的中文注释

这样做的好处

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

相关推荐
Bryce李小白13 分钟前
Kotlin Flow 的使用
android·开发语言·kotlin
氦客2 小时前
Android Compose 状态的概念
android·compose·重组·状态·组合·mutablestate·mutablestateof
Jerry3 小时前
Compose 约束条件和修饰符顺序
android
千里马学框架4 小时前
安卓系统中线程优先级Priority查看方式汇总
android·framework·线程·安卓framework开发·优先级·priority
沐怡旸4 小时前
【Android】Handler/Looper机制相关的类图和流程图
android
生莫甲鲁浪戴4 小时前
Android Studio新手开发第二十一天
android·ide·android studio
生莫甲鲁浪戴4 小时前
Android Studio新手开发第二十二天
android·ide·android studio
用户41659673693555 小时前
Jetpack Compose 中实现带圆角边框的单词级富文本效果(分词与布局实践)
android
顾林海5 小时前
Android UI优化:让你的APP从“卡顿掉帧”到“丝滑如德芙”
android·面试·性能优化
啊森要自信5 小时前
【MySQL 数据库】MySQL用户管理
android·c语言·开发语言·数据库·mysql