Android UltimateRecyclerView优雅地使用RecyclerView

转载请标明出处:

http://blog.csdn.net/xuehuayous/article/details/51158759;

本文出自:【Kevin.zhou的博客】

前言:大家都在更青睐于使用RecyclerVIew来替代ListView,但是在使用的时候我们会发现ListView的一些常用方法在RecyclerView中没有,比如添加头部、尾部。而且在刷新加载方面ListView的封装也比较多,如我之前常用的PullToRefreshListView,之前也在PullToRefresh项目上做过支持RecyclerView,但是效果还是不能让人满意。于是想写一个好用的RecyclerView来方便开发中使用。

一、 最终效果

按照以前博客的风格,我们先看下最终实现的效果:

二、 需求

我们希望做可以做成这样的,可以刷新加载,可以添加头部尾部。并且在使用的时候要贴近ListView的用法,不要让使用者去学习它怎么去使用。

三、需求分析

根据以上,刷新加载选取PullToRefresh为基础以及指导思想。为什么使用PullToRefresh?站在巨人的肩膀上,而且对它的源码比较了解,没有看过我之前博客的朋友可以看下 《 Android PullToRefresh 完全解析》,这里分为了五篇博客来从原理上详细介绍PullToRefresh框架。

看过《Android PullToRefresh 分析之四、扩展RecyclerView》的朋友知道之前已经在PullToRefresh上做了简单的支持RecyclerView,有些朋友在使用的时候提出了一些问题,比如在刷新加载的时候RecyclerView不能滚动,这个问题要想解决会比较复杂,扩展RecyclerView,使其支持添加头部尾部。于是在这篇博客中《Android RecyclerView添加头部和尾部》介绍了封装的可以添加头部尾部的RecyclerView。

四、实现思路

由于PullToRefresh的设计为在LinearLayout中添加刷新头部、内容区域、加载尾部,如下图:

所以就导致了头部或者尾部显示的时候不能去让中间的内容区域滚动,有的朋友就会说那PullToRefreshListView是如何实现的呢,它的实现就是在有两个刷新头部和两个加载尾部,听起来不可思议,由于ListView可以添加头部尾部,就在ListView的头部添加一个刷新头部,在ListView的尾部添加一个刷新尾部,平常的时候是隐藏的,只有刷新加载动作促发的时候,将其显示,并把原来的头部隐藏。

在正常显示到开始下拉,将要促发刷新状态,这些过程中添加到ListView头部的刷新头部都是隐藏的。

在加载数据的时候,原来的头部隐藏,添加到ListView头部的加载提示View显示。

总结一下状态的切换,如下图所示:

分析了那么多PullToRefreshListView,我们的RecyclerView也可以这么实现的。

五、实现步骤

1. 继承PullToRefreshBase,实现抽象方法,生成构造

public class UltimateRecyclerView extends PullToRefreshBase<WrapRecyclerView> {

    public UltimateRecyclerView(Context context) {
        super(context);
    }

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

    public UltimateRecyclerView(Context context, Mode mode) {
        super(context, mode);
    }

    public UltimateRecyclerView(Context context, Mode mode, AnimationStyle animStyle) {
        super(context, mode, animStyle);
    }

    @Override
    public Orientation getPullToRefreshScrollDirection() {
        return null;
    }

    @Override
    protected WrapRecyclerView createRefreshableView(Context context, AttributeSet attrs) {
        return null;
    }

    @Override
    protected boolean isReadyForPullEnd() {
        return false;
    }

    @Override
    protected boolean isReadyForPullStart() {
        return false;
    }
}

2. 设置刷新方向为竖向

@Override
public final Orientation getPullToRefreshScrollDirection() {
    return Orientation.VERTICAL;
}

3. 设置刷新的View

这里是封装的可以添加头部尾部的 WarpRecyclerView

@Override
protected WrapRecyclerView createRefreshableView(Context context, AttributeSet attrs) {
    WrapRecyclerView recyclerView = new InternalWrapRecyclerView(context, attrs);
    recyclerView.setId(R.id.ultimate_recycler_view);
    return recyclerView;
}

这里为什么使用了一个InternalWrapRecyclerView,这个后面再讲。

4. 设置判断是否到顶部

@Override
protected boolean isReadyForPullStart() {
    return isFirstItemVisible();
}

5. 设置判断是否到底部

@Override
protected boolean isReadyForPullEnd() {
    return isLastItemVisible();
}

由于这里在简单扩展PullToRefreshRecyclerView 《Android PullToRefresh 分析之四、扩展RecyclerView》中有详细说明,这里简单一带而过。

6. 在RecyclerView添加刷新头部和初始化加载尾部

private void init(Context context, AttributeSet attrs) {

    // Styleables from XML
    TypedArray ua = context.obtainStyledAttributes(attrs, R.styleable.UltimateRecyclerView);
    mURecyclerViewExtrasEnabled = ua.getBoolean(R.styleable.UltimateRecyclerView_ptrURecyclerViewExtrasEnabled, true);
    ua.recycle();

    // Styleables from XML
    TypedArray pa = context.obtainStyledAttributes(attrs, com.handmark.pulltorefresh.library.R.styleable.PullToRefresh);
    if (mURecyclerViewExtrasEnabled) {
        final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
        FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
        final ViewGroup.LayoutParams hlp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.WRAP_CONTENT);

        // Create Loading Views ready for use later
        mSvHeaderLoadingFrame = new FrameLayout(getContext());
        mHeaderLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_START, pa);
        mHeaderLoadingView.setVisibility(View.GONE);
        mSvHeaderLoadingFrame.addView(mHeaderLoadingView, lp);
        mSvHeaderLoadingFrame.setLayoutParams(hlp);
        mRefreshableView.addHeaderView(mSvHeaderLoadingFrame);

        mSvFooterLoadingFrame = new FrameLayout(getContext());
        mFooterLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_END, pa);
        mFooterLoadingView.setVisibility(View.GONE);
        mSvFooterLoadingFrame.addView(mFooterLoadingView, lp);
        mSvFooterLoadingFrame.setLayoutParams(hlp);

        mSvSecondFooterLoadingFrame = new FrameLayout(getContext());
        mSvSecondFooterLoadingFrame.setLayoutParams(hlp);

        pa.recycle();

    }
}

细心的朋友会发现,这里只有通过mRefreshableView.addHeaderView(mSvHeaderLoadingFrame);添加了刷新头部到RecyclerView,并没有添加加载尾部到RecyclerView1呀,对的。是因为在没有设置RecyclerView的Adapter之前我们不希望加载尾部会出现,因为这时候没有意义。在上文提到的InternalWrapRecyclerView中去设置;

7. 在RecyclerView添加加载尾部

protected class InternalWrapRecyclerView extends WrapRecyclerView {

    private boolean mAddedSvFooter = false;

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

    @Override
    public void setAdapter(Adapter adapter) {
        // Add the Footer View at the last possible moment
        if (null != mSvFooterLoadingFrame && !mAddedSvFooter) {
            addFooterView(mSvFooterLoadingFrame);
            mAddedSvFooter = true;
        }
        super.setAdapter(adapter);
    }

}

8. 在刷新的时候隐藏原来的头部尾部,显示添加到RecyclerView的头部尾部

@Override
protected void onRefreshing(final boolean doScroll) {
    WrapAdapter adapter = mRefreshableView.getAdapter();
    if (!mURecyclerViewExtrasEnabled || !getShowViewWhileRefreshing() || null == adapter || adapter.getItemCount() == 0) {
        super.onRefreshing(doScroll);
        return;
    }

    super.onRefreshing(false);

    final LoadingLayoutBase origLoadingView, recyclerViewLoadingView, oppositeRecyclerViewLoadingView;
    final int scrollToPosition, scrollToY;

    switch (getCurrentMode()) {
        case MANUAL_REFRESH_ONLY:
        case PULL_FROM_END:
            origLoadingView = getFooterLayout();
            recyclerViewLoadingView = mFooterLoadingView;
            oppositeRecyclerViewLoadingView = mHeaderLoadingView;
            scrollToPosition = mRefreshableView.getBottom();
            scrollToY = getScrollY() - getFooterSize();
            break;
        case PULL_FROM_START:
        default:
            origLoadingView = getHeaderLayout();
            recyclerViewLoadingView = mHeaderLoadingView;
            oppositeRecyclerViewLoadingView = mFooterLoadingView;
            scrollToPosition = mRefreshableView.getTop();
            scrollToY = getScrollY() + getHeaderSize();
            break;
    }

    // 隐藏原来的加载View
    origLoadingView.reset();
    origLoadingView.hideAllViews();

    // 刷新时隐藏尾部,加载时隐藏头部
    oppositeRecyclerViewLoadingView.setVisibility(View.GONE);

    // 设置RecyclerView内的加载View显示并设置它为刷新状态
    recyclerViewLoadingView.setVisibility(View.VISIBLE);
    recyclerViewLoadingView.refreshing();

    if (doScroll) {
        // We need to disable the automatic visibility changes for now
        disableLoadingLayoutVisibilityChanges();

        // 刷新布局由过度滑动状态恢复
        setHeaderScroll(scrollToY);

        // 让添加到RecyclerView的刷新头部或者加载尾部显示出来
        mRefreshableView.smoothScrollToPosition(scrollToPosition);

        // 把整体滚回初始位置
        smoothScrollTo(0);
    }
}

通过注释就可以了解执行的过程,在刷新加载数据的时候,我们添加到RecyclerView的就显示出来接管了原来的刷新加载布局。

9. 刷新加载完成恢复状态

@Override
protected void onReset() {
    if (!mURecyclerViewExtrasEnabled) {
        super.onReset();
        return;
    }

    final LoadingLayoutBase originalLoadingLayout, recyclerViewLoadingLayout;
    final int scrollToHeight, selection;
    final boolean scrollSvToEdge;

    WrapAdapter adapter = mRefreshableView.getAdapter();

    switch (getCurrentMode()) {
        case MANUAL_REFRESH_ONLY:
        case PULL_FROM_END:
            originalLoadingLayout = getFooterLayout();
            recyclerViewLoadingLayout = mFooterLoadingView;
            selection = adapter.getItemCount() - 1;
            scrollToHeight = getFooterSize();
            scrollSvToEdge = Math.abs(getLastVisiblePosition() - selection) <= 1;
            break;
        case PULL_FROM_START:
        default:
            originalLoadingLayout = getHeaderLayout();
            recyclerViewLoadingLayout = mHeaderLoadingView;
            scrollToHeight = -getHeaderSize();
            selection = 0;
            scrollSvToEdge = Math.abs(getFirstVisiblePosition() - selection) <= 1;
            break;
    }

    // 如果添加到RecyclerView的加载布局在显示
    if (recyclerViewLoadingLayout.getVisibility() == View.VISIBLE) {

        // 显示原来的加载布局
        originalLoadingLayout.showInvisibleViews();

        // 隐藏添加到RecyclerView的加载布局
        recyclerViewLoadingLayout.setVisibility(View.GONE);

// 滚动隐藏头部或者尾部
        if (scrollSvToEdge && getState() != State.MANUAL_REFRESHING) {
            mRefreshableView.scrollToPosition(selection);
            setHeaderScroll(scrollToHeight);
        }
    }

    super.onReset();
}

重置加载刷新状态,简单说就是收拾刷新加载形成的烂摊子。

OK,通过以上操作就能实现刷新加载的时候也可以滚动RecyclerView啦。

六、优化加载尾部

我们大小好多程序的效果是这样的,到尾部的时候就自动加载,不需要再往上滑一下。这里需要监听是否是滑动到了最后一个。原理也比较简单。

public final void setOnLastItemVisibleListener(OnLastItemVisibleListener listener) {
    mOnLastItemVisibleListener = listener;
}

看一下定义的接口

public static interface OnLastItemVisibleListener {

    public void onLastItemVisible();
}

是不是干净利索,最简单的一个回调。

重点就是怎么判断到最后一个:

/**
 * 判断最后一个条目是否能够可见
 *
 * @return boolean:
 * @version 1.0
 * @date 2016-4-12 14:51:04
 * @Author zhou.wenkai
 */
private boolean isLastItemVisible() {
    final RecyclerView.Adapter<?> adapter = getRefreshableView().getAdapter();
    // 如果未设置Adapter,都没有添加自然不可见
    if(null == adapter) {
        return false;
    } else {
        // 最后一个条目View是否展示
        int lastVisiblePosition = getLastVisiblePosition();

        // 最后一个显示出来了
        if(lastVisiblePosition == mRefreshableView.getAdapter().getItemCount() - 2) {
            // 说明最后一个刚刚显示出来
            // 这里不希望和PullToRefreshListView中一样只要最后一个显示,每动一下就促发一次回调
            if(lastVisiblePosition == mTmplastVisiblePosition + 1) {
                mTmplastVisiblePosition = lastVisiblePosition;
                    return true;
            }
        }
        mTmplastVisiblePosition = lastVisiblePosition;
    }
    return false;
}
private int getLastVisiblePosition() {
    View lastVisibleChild = mRefreshableView.getChildAt(mRefreshableView
.getChildCount() - 1);
    return lastVisibleChild != null ? mRefreshableView
.getChildAdapterPosition(lastVisibleChild) : -1;
}

判断是否最后一个显示的方法有了,那么只要实时的监控就可以啦。监控可以写在 onScroll的回调中:

@Override
public void onScrolled(int dx, int dy) {
    super.onScrolled(dx, dy);
    boolean lastItemVisible = isLastItemVisible();
    if(lastItemVisible) {
        mOnLastItemVisibleListener.onLastItemVisible();
    }
}

七、添加修改刷新头部、加载尾部

扩展刷新加载样式在《Android PullToRefresh 分析之五、扩展刷新加载样式》有详细介绍原理,这里只是把代码列出来。

1.
设置头部刷新布局

@Override
public void setHeaderLayout(LoadingLayoutBase headerLayout) {
    super.setHeaderLayout(headerLayout);

    try {
        Constructor c = headerLayout.getClass().getDeclaredConstructor(new Class[]{Context.class});
        LoadingLayoutBase mHeaderLayout = (LoadingLayoutBase)c.newInstance(new Object[]{getContext()});
        if(null != mHeaderLayout) {
            mSvHeaderLoadingFrame.removeAllViews();
            final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);

            mHeaderLoadingView = mHeaderLayout;
            mHeaderLoadingView.setVisibility(View.GONE);
            mSvHeaderLoadingFrame.addView(mHeaderLoadingView, lp);
            mRefreshableView.getAdapter().notifyDataSetChanged();
       }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2. 设置尾部加载布局

@Override
public void setFooterLayout(LoadingLayoutBase footerLayout) {
    super.setFooterLayout(footerLayout);

    try {
        Constructor c = footerLayout.getClass().getDeclaredConstructor(new Class[]{Context.class});
        LoadingLayoutBase mFooterLayout = (LoadingLayoutBase)c.newInstance(new Object[]{getContext()});
        if(null != mFooterLayout) {
            mSvFooterLoadingFrame.removeAllViews();
            final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);

            mFooterLoadingView = mFooterLayout;
            mFooterLoadingView.setVisibility(View.GONE);
            mSvFooterLoadingFrame.addView(mFooterLoadingView, lp);
            mRefreshableView.getAdapter().notifyDataSetChanged();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

自定义的刷新加载布局由于使用了反射创建实例,意义要保障有一个带有Context的构造函数。

八、添加设置尾部加载提示

我们好多时候需要在数据加载完成的时候提示用户没有更多数据啦,有了可以添加尾部的RecyclerView这个就比较简单了。我们只需要再在尾部添加一个View就可以啦。

@Override
public void setAdapter(Adapter adapter) {
    // Add the Footer View at the last possible moment
    if (null != mSvFooterLoadingFrame && !mAddedSvFooter) {
        addFooterView(mSvSecondFooterLoadingFrame);
        addFooterView(mSvFooterLoadingFrame);
        mAddedSvFooter = true;
    }
    super.setAdapter(adapter);
}

在原来添加尾部的地方再加一个提示布局,然后提供一个设置布局的方法:

@Override
public void setSecondFooterLayout(View secondFooterLayout) {
    final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
    FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);

    mSvSecondFooterLoadingFrame.addView(secondFooterLayout, lp);
}

九、简单使用

1. 在xml中添加控件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ptr="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.kevin.ultimaterecyclerview.UltimateRecyclerView
        android:id="@+id/main_act_urv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        ptr:ptrMode="pullFromStart">
    </com.kevin.ultimaterecyclerview.UltimateRecyclerView>
</RelativeLayout>

2. 代码中初始化

mUltimateRecyclerView = (UltimateRecyclerView) this.findViewById(R.id.main_act_urv);
// 设置头部刷新样式为自定义
mUltimateRecyclerView.setHeaderLayout(new TmallHeaderLayout(this));

3. 获取RecyclerView

WrapRecyclerView mWrapRecyclerView = mUltimateRecyclerView.getRefreshableView();

这里的WrapRecyclerView是对RecyclerView可以添加头部尾部的封装。

4. 添加RecyclerView头部(视需求而定)

LayoutInflater inflater = LayoutInflater.from(this);
FrameLayout layout = (FrameLayout) inflater.inflate(R.layout.recycler_header, null);
mAdLoopView = (AdLoopView) layout.findViewById(R.id.main_act_alv);
mWrapRecyclerView.addHeaderView(layout);

5. 设置刷新监听

// 设置刷新监听
mUltimateRecyclerView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener<WrapRecyclerView>() {
    @Override
    public void onRefresh(PullToRefreshBase<WrapRecyclerView> refreshView) {
        new GetDataTask(true).execute();
    }
});
// 设置最后一个条目可见监听
mUltimateRecyclerView.setOnLastItemVisibleListener(new PullToRefreshBase.OnLastItemVisibleListener() {
    @Override
    public void onLastItemVisible() {
    boolean hasMoreData = secondFooterLayout.isHasMoreData();
    Log.i("", "是否还有更多数据 " + hasMoreData);
        if(hasMoreData) {
            new GetDataTask(false).execute();
        }
    }
});

使用和原PullToRefresh框架基本一致,这里就不再赘述,详细的使用请参考本项目的示例,以及以及原项目示例

十、源码及示例

给大家提供一个github的地址: Android-UltimateRecyclerView

另外,欢迎 star or f**k me on github!  

十一、一行引入库

如果您的项目使用 Gradle 构建, 只需要在您的build.gradle文件添加下面一行到 dependencies :

compile ‘com.kevin:ultimaterecyclerview:1.0.1‘
时间: 2024-10-08 07:20:18

Android UltimateRecyclerView优雅地使用RecyclerView的相关文章

Android开发学习之路-RecyclerView滑动删除和拖动排序

Android开发学习之路-RecyclerView使用初探 Android开发学习之路-RecyclerView的Item自定义动画及DefaultItemAnimator源码分析 Android开发学习之路-下拉刷新怎么做? 本篇是接着上面三篇之后的一个对RecyclerView的介绍,这里多说两句,如果你还在使用ListView的话,可以放弃掉ListView了.RecyclerView自动帮我们缓存Item视图(ViewHolder),允许我们自定义各种动作的动画和分割线,允许我们对It

【Android 界面效果47】RecyclerView详解

RecylerView作为 support-library发布出来,这对开发者来说绝对是个好消息.因为可以在更低的Android版本上使用这个新视图.下面我们看如何获取 RecylerView.首先打开Android SDK Manager,然后更新Extras->Android Support Library即可. 然后在本地../sdk/extras/android/support/v7中找到recyclerview.我已经将下载好的Recyclerview整理成一个Eclipse可编译的L

android L新控件RecyclerView详解与DeMo

介绍 在谷歌的官网我们可以看到它是这样介绍的:RecyclerView is a more advanced and flexible version of ListView. This widget is a container for large sets of views that can be recycled and scrolled very efficiently. Use the RecyclerView widget when you have lists with eleme

基于Android官方AsyncListUtil优化改进RecyclerView分页加载机制(一)

基于Android官方AsyncListUtil优化改进RecyclerView分页加载机制(一) Android AsyncListUtil是Android官方提供的专为列表这样的数据更新加载提供的异步加载组件.基于AsyncListUtil组件,可以轻易实现常见的RecyclerView分页加载技术.AsyncListUtil技术涉及的细节比较繁复,因此我将分别写若干篇文章,分点.分解AsyncListUtil技术. 先给出一个可运行的例子,MainActivity.java: packag

基于Android官方Paging Library的RecyclerView分页加载框架

基于Android官方Paging Library的RecyclerView分页加载框架 我之前写了一篇RecyclerView分页加载机制的文章,是基于Android官方的AsyncListUtil实现的,详情见附录文章1.现在再介绍一种RecyclerView分页加载框架:Android Paging Library.Android Paging Library是Android官方support-v7支持包中专门做的分页框架,详细文档见谷歌官方文档附录2页面.我写这篇文章时候Paging L

Android Material Design学习之RecyclerView代替 ListView

前言 # Android Material Design越来越流行,以前很常用的 ListView 现在也用RecyclerView代替了,实现原理还是相似的.笔者实现一下 RecyclerView,代码比较简单,适合初学者,如有错误,欢迎指出. 源码地址(欢迎star) https://github.com/studychen/SeeNewsV2 本文链接 http://blog.csdn.net/never_cxb/article/details/50495505,转载请注明出处. 复习 L

Android Material Design之在RecyclerView中嵌套CardView实现

前言: 第一眼就爱上了Android的Material Design风格.以前倒对Android的界面风格不那么喜欢,扁平化的界面设计真是好看. 其实,这个嵌套操作在实现上并没有什么难点.可是,我还在Eclipse上没有试验成功.后来换到Android Studio上就OK了.以下是实现过程. 笔者开发环境: 系统:Windows 7 旗舰版 IDE:Android Studio v1.0 工具包:cardview-v7-21.0.0.aar recyclerview-v7-21.0.0.aar

【Android开源项目解析】RecyclerView侧滑删除粒子效果实现——初探Android开源粒子库 Leonids

前两天在微博上看到了这个侧滑删除的粒子效果,但是只有IOS的,所以心血来潮,写了个玩玩,下面简单介绍下实现的思路 项目简介 实现原理解析 代码实现 如何使用 更多参考 项目简介 先不废话,上效果图 项目地址:https://github.com/ZhaoKaiQiang/ParticleLayout 实现原理解析 其实看了那么多的关于侧滑删除的项目,再来思考这个问题,就so easy了! 咱们先分析下需求: - 侧滑手势检测 - 粒子跟手效果 - 删除状态判断 - 数据源刷新 ok,知道需求了,

Android TV端的(RecyclerView)水平滚动焦点错乱问题

package com.hhzt.iptv.ui.customview; import android.content.Context;import android.content.res.TypedArray;import android.graphics.Rect;import android.os.Build;import android.support.v4.view.ViewCompat;import android.support.v7.widget.GridLayoutManage