Android UIAndroid

自定义 RecyclerView 实现刷新与加载更多

2018-01-07  本文已影响2383人  程序杂念

“哎呀,最近新接手一个项目里要用 RecyclerView 的地方好多,而且基本上都需要上拉刷新和下拉加载,我才写了两个页面,手都快断了,有没有什么比较好的方法?”

“什么!你还在自己一点点地写?那你得写到啥时候去?”

“那怎么办?”

“你是不是傻,Github 上那么多优秀的开源库,用一个啊。或者是自己写一个简单的,别写那么复杂的,能满足自己需求的就行。”

“有道理,我去看看”

......

“Github 上的优秀的好多啊,我都看花了眼,不知道该选哪一个?”

“实在不行,你就自己写一个呗,如果只是实现你的需求应该不难,来说说看,你的需求是什么?”

“也不是很复杂,就简单的一个,能上拉加载更多,下拉刷新的就行,简单不?”

“你出去别说你是程序员,会被怼死的……”

“咋了,这不行吗?”

“不是不行,你这需求提了跟没提一个样,有没有点实质性的东西?”

“嗯……,那我重说

  1. 在布局文件中只使用一个控件,不做任何的嵌套;
  2. 在 RecyclerView 没有数据的时候,页面上显示提示信息,‘暂无数据’ 就行;
  3. 支持下拉刷新,刷新显示的 Header 就用官方的那个小圆圈就行了;
  4. 支持上拉加载,如果有下一页数据, Footer 显示一个 Loading 配上‘加载中……’,如果没有数据,就显示‘你已经扯到底了’。

嗯,就这么多。”

“还行,也不算难,有什么想法吗?”

“暂时没了,后面想到再加吧。”

“…………”

“那行,那我们先来看看具体怎么实现吧。”

“1. 同一个控件内既能刷新又要可以上拉加载,官方的控件应该是没有的,那就自定义一个吧,自定义一个 ViweGroup ,然后把 SwipeRefreshLayout 和 RecyclerView 包裹起来,然后再给 RecyclerView 添加一个滑动监听事件;

  1. 在没有数据的情况下显示提示信息,这个可以放一个 TextView 或者是 ImageView ,然后在获取数据之后,判断是否为空,如果数据不为空就显示 RecyclerView,如果数据为空,就显示 TextView;
  2. 需要动态的修改 Footer 显示的内容,那就在 RecyclerView 的 Adapter 中动态更改呗。

“等等,那个 Footer 的动态更改能不能不让我做啊,每次都要写动态改变代码,实在不想写。”

“没让你写啊,我是说放在我们内部的 Adapter 里面。是这样的,我打算自己实现一个 Adapter ,在这个 Adapter 里面去动态的更新 Footer。”

“你实现了 Adapter,那我外面还能用吗?

“可以啊,咋不行,我自定义的 Adapter 把你外层需要用到的 Adapter 包裹起来,你还是你,不过,你外面还有一层。玩过俄罗斯套娃吗?”

“没有……”

“想想手机和手机壳的关系,这个跟那个类似。”

“好像懂了……”

“那我们来实现吧。”

“从哪下手啊?我怎么感觉一脸懵逼。”

“先从布局文件开始,先来写显示出来的列表布局,在里面写上你要的那几个控件。然后再给 Footer 来一个布局。”

me_recyclerview.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="match_parent">

    <android.support.v7.widget.AppCompatTextView
        android:id="@+id/list_tip_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="没有数据"
        android:textSize="16sp" />

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/list_swipe_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="gone" />

    </android.support.v4.widget.SwipeRefreshLayout>

</FrameLayout>

“嗯,这就是列表布局了,内容很简单就是你现在写的样子……,好了,再来写个 Footer 布局。”

“等等,这个简单,我来。”

“行行行,你来。”

me_foot.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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">

    <ProgressBar
        android:id="@+id/item_footer_progress"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.4"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/item_footer_message"
        android:layout_width="wrap_content"
        android:layout_height="48dp"
        android:layout_marginStart="8dp"
        android:gravity="center"
        android:text="加载中"
        app:layout_constraintLeft_toRightOf="@+id/item_footer_progress" />

</android.support.constraint.ConstraintLayout>

“还行,那我们继续?”

“好嘞。”

“刚才说了,需要使用一个 Adapter 来包裹外层调用方的 Adapter ,那么还需要自定义一个 Adapter ,就叫 MeRefreshListAdapter 吧。来继续”

MeRefreshListAdapter.java

    private static final int TYPE_FOOTER = -1;
    private RecyclerView.Adapter adapter;
    private LayoutInflater inflater;
    private boolean isShowFooter;
    private boolean isNoData;

    public MeRefreshListAdapter(RecyclerView.Adapter adapter, Context context, boolean isShowFooter, boolean isNoData) {
        this.adapter = adapter;
        this.isShowFooter = isShowFooter;
        this.isNoData = isNoData;
        inflater = LayoutInflater.from(context);
    }

“你这里那个 RecyclerView.Adapter 干啥使得?”

“那个就是外面你传进来的 Adapter 啊,你外面的 Adapter 肯定得继承自 RecyclerView.Adapter 吧,那我得保证,你加的什么泛型我这边都能用啊。比如你有一个是继承自 RecyclerView.Adapter<Person.ViewHolder>,还有一个是继承自 RecyclerView.Adapter<City.ViewHolder>”。

“哦~”

“还有,这个 MeRefreshListAdapter 需要控制 Footer 和外层 Adapter 的 Item 的创建与绑定,总的来说,他们是两类,我不管你外层的 Item 的类型有几个,我都要加上 1 ,这个 1 就是 Footer。并且,我内部只对 Footer 这一个类型的 Item 进行控制,其余的还是交给你外层去做。不过这时候就需要进行判断了,到底哪一个是 Footer 类型的,哪一个是普通类型(外层的 Item 类型)。”

“这个我知道,简单,Footer 肯定是在最后一个的,那就直接把最后一个归为 Footer 就行了,不对,当不显示 Footer 的时候,最后一个 Item 也是普通类型,那就是在显示 Footer 的情况下的最后一个 Item 是 Footer。”

“嗯,是的,这样一来,我们自定义的 Adapter 也就出来了。”

MeRefreshListAdapter.java

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder;
        switch (viewType) {
            case TYPE_FOOTER:
                View footer = inflater.inflate(R.layout.me_list_footer, parent, false);
                viewHolder = new FooterHolder(footer);
                break;
            default:
                viewHolder = adapter.onCreateViewHolder(parent, viewType);
                break;
        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof FooterHolder) {
            FooterHolder footerHolder = (FooterHolder) holder;
            footerHolder.progressBar.setVisibility(isNoData ? View.GONE : View.VISIBLE);
            footerHolder.message.setText(isNoData ? "--- 扯到底了 ---" : "加载中……");
        }else{
            adapter.onBindViewHolder(holder,position);
        }
    }

    @Override
    public int getItemCount() {
        return isShowFooter ? adapter.getItemCount() + 1 : adapter.getItemCount();
    }

    @Override
    public int getItemViewType(int position) {
        if (isShowFooter && position + 1 == getItemCount()) {
            return TYPE_FOOTER;
        } else {
            return adapter.getItemViewType(position);
        }
    }
    
    static class FooterHolder extends RecyclerView.ViewHolder {
        ProgressBar progressBar;
        TextView message;

        FooterHolder(View itemView) {
            super(itemView);
            progressBar = itemView.findViewById(R.id.item_footer_progress);
            message = itemView.findViewById(R.id.item_footer_message);
        }
    }

    public void setShowFooter(boolean flag) {
        this.isShowFooter = flag;
        this.notifyDataSetChanged();
    }

    public void setNoData(boolean flag) {
        this.isNoData = flag;
        this.notifyDataSetChanged();
    }

“当然了,还需要对外开放更新 Adapter 的方法,这样才能实时控制嘛。”

“我们接下来干啥?”

“写接口,我们需要自定义两个接口方法,用来在外层调用。很简单,就下面这样的。”

RefreshLoadListener.java

public interface RefreshLoadListener {
    /**
     * 上拉加载更多
     */
    void upLoad();

    /**
     * 下拉刷新
     */
    void downRefresh();
}

“接口也写完了,现在开始进入正题了。自定义 ViewGroup 继承自 LinearLayout,然后重写其中的构造方法。四个构造方法都要重写啊,不写的话可能会运行报错。”

MeRecyclerView.java

public class MeRecyclerView extends LinearLayout implements SwipeRefreshLayout.OnRefreshListener {

    private SwipeRefreshLayout refreshLayout;
    private RecyclerView refreshList;
    private AppCompatTextView listTip;
    private RefreshLoadListener loadListener;
    private MeRefreshListAdapter meRefreshListAdapter;
    private Context mContext;
    private RecyclerView.LayoutManager layoutManager;
    private boolean isShowFooter;
    private boolean isNoData;
    private int lastVisibleItem;

    public MeRecyclerView(Context context) {
        super(context);
        init(context);
    }

    public MeRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
    public MeRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public MeRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }
    
    private void init(Context context) {
        this.mContext = context;
        LayoutInflater.from(context).inflate(R.layout.me_recyclerview, this, true);
        refreshLayout = findViewById(R.id.list_swipe_refresh);
        listTip = findViewById(R.id.list_tip_message);
        refreshList = findViewById(R.id.list_list);

        refreshLayout.setOnRefreshListener(this);
    }
}

“你看,刚才在初始化操作的时候,我们就设置了 SwipeRefreshLayout 的刷新接口,现在就到了我们需要实现他的时候了,可能跟你平时写的不太一样,因为虽然我们实现了这个接口,但我们还是得把这个具体实现的机会交给外层,让外层进行实际的数据获取。当然了,我们如果当前页面处于刷新状态,那 Footer 肯定是不能显示出来的,这个时候就需要操作一下刚才我们写的 MeRefreshListAdapter ”

MeRecyclerView.java

    @Override
    public void onRefresh() {
        if (null != loadListener) {
            isNoData = false;
            isShowFooter = false;
            meRefreshListAdapter.setNoData(isNoData);
            meRefreshListAdapter.setShowFooter(isShowFooter);
            loadListener.downRefresh();
        }
    }

“好了,下拉刷新结束了,是不是很简单?”

“这就结束了,这也太快了。”

“下面来看上拉加载更多。”

“刷新的容易,加载更多的,要控制下面 Footer 显示还要改变显示的文字,想想头就大。”

“你想啥呢,那些都做完了啊,刚在在 MeRefreshListAdapter 不是就做过了……”

“完全没意识到……”

“上拉加载其实跟刷新一样的,都挺好实现的,无非就是给 RecyclerView 添加一个滑动监听,然后再根据当前位置去判断是否加载新数据。你看”

MeRecyclerView.java

        refreshList.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                int totalCount = layoutManager.getItemCount() - 1;
                if (totalCount > 18 && newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem == totalCount && !isNoData && !isShowFooter) {
                    if (null != loadListener) {
                        setFooter();
                        loadListener.upLoad();
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                if (layoutManager instanceof LinearLayoutManager) {
                    lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
                }
            }
        });
        
    private void setFooter() {
        isShowFooter = true;
        meRefreshListAdapter.setShowFooter(true);
    }

"好了,上拉加载更多也解决了,不过这里我偷了个懒。在获取当前列表的最后一项的 position 时,我判断了 LinearLayoutManager ,其他的没进行判断,不过也简单,GridLayoutManager 是继承自 LinearLayoutManager ,而 StaggeredGridLayoutManager 获取最后一个 item 的 position 返回的是一个数组,取出其中最大的即可。"

“好吧,那这样是不是就算完成了啊?”

“想多了,看起来,我们是做完这么多的,但是,并没有做完,比如,我们还需要进行停止刷新,要不然那个无论是上拉加载还是刷新都会有那个 loading 在那不停地动。”

MeRecyclerView.java

    public void stopRefresh(int pageCount, boolean isNoData) {
        this.isNoData = isNoData;
        meRefreshListAdapter.setNoData(isNoData);
        showData(meRefreshListAdapter.getItemCount() > 0);
        if (pageCount == 1) {
            refreshLayout.setRefreshing(false);
        } else {
            if (!isNoData) {
                isShowFooter = false;
                meRefreshListAdapter.setShowFooter(isShowFooter);
            }
        }
    }
    
    private void showData(boolean b) {
        refreshList.setVisibility(b ? VISIBLE : VISIBLE);
        listTip.setVisibility(b ? GONE : VISIBLE);
    }

"再比如,我们还需要与外部的 LayoutManager 以及 Adapter 建立联系以及在外部数据变动的时候,通知我们进行刷新"

MeRecyclerView.java

    public void setLayoutManager(RecyclerView.LayoutManager layoutManager) {
        this.layoutManager = layoutManager;
        refreshList.setLayoutManager(layoutManager);
    }

    public void setAdapter(RecyclerView.Adapter adapter) {
        meRefreshListAdapter = new MeRefreshListAdapter(adapter, mContext, isShowFooter, isNoData);
        refreshList.setAdapter(meRefreshListAdapter);
    }
    
    public void notifyDataSetChanged() {
        meRefreshListAdapter.notifyDataSetChanged();
    }

"这还不算完……"

“还没完?”

“肯定的啊,你想啊,页面刚打开的时候,你是不是得立即去刷新一下,做事总得主动点嘛,所以我们还需要一个可以开始刷新的。”

“我知道,是让页面进来的时候去刷新一下,开始获取数据对吧,我们可以直接调用下拉刷新的方法啊,那样最省事。就像这样。”

    public void startRefresh() {
        refreshLayout.setRefreshing(true);
        onRefresh();
    }

“是的,这么样就行了。不过还有一个最最最重要的就是得让外部把接口实现了,所以还要这么一个方法。”

    public void setLoadListener(RefreshLoadListener loadListener) {
        this.loadListener = loadListener;
    }

"这个,我肯定不会晚啊,不过如果忘记了,肯定都不能用啊。我先拿去试试看好不好用啊。"

“……”

“真爽,用起来特简单。在布局文件里面只用一个控件,就是刚才我们自定义的那个,然后,像往常一样设置 LayoutManager 和 Adapter 不过需要在服务器返回数据之后自己手动停止刷新。还是挺简单的。哦,对,还有一个要自己启动刷新。”

RefreshActivity.java

    @BindView(R.id.refresh_demo_list)
    MeRecyclerView refreshDemoList;
    int page = 1;
    int size = 20;
    List<Map<String, Object>> dataList;
    ListAdapter listAdapter;
    LinearLayoutManager layoutManager;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_refresh);
        ButterKnife.bind(this);

        dataList = new ArrayList<>();
        listAdapter = new ListAdapter(this, R.layout.item_simgle_check, dataList);
        layoutManager = new LinearLayoutManager(this);
        
        refreshDemoList.setLoadListener(this);
        refreshDemoList.setLayoutManager(layoutManager);
        refreshDemoList.setAdapter(listAdapter);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        refreshDemoList.startRefresh();
    }
    
    @Override
    public void downRefresh() {
        page = 1;
        getData();
    }

    @Override
    public void upLoad() {
        page += 1;
        getData();
    }
    
    private void update(List<Map<String, Object>> maps) {
        if (page > 5) {
            maps.clear();
        }
        if (maps.size() > 0) {
            if (page == 1) {
                dataList.clear();
                dataList.addAll(maps);
            } else {
                dataList.addAll(maps);
            }
            refreshDemoList.notifyDataSetChanged();
            refreshDemoList.stopRefresh(page, false);
        } else {
            refreshDemoList.stopRefresh(page, true);
        }
    }

没有数据 加载更多 没有更多

"好了,跟你学完了,我还去继续写代码了,还有什么问题,下次再来问你。希望有一天,我能不问你也能把问题解决了。👍"

上一篇 下一篇

猜你喜欢

热点阅读