打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(二)

来源:互联网 发布:java数组转字符串 编辑:程序博客网 时间:2024/06/06 12:35

在开始本篇阅读前,建议大家先看下上一篇《 打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(一)》,主要是由于本篇很多接口和设计都在上一篇提到,在这里不会做过多展开。

在本系列的上一篇文章中,我们为大家分析了整个下拉刷新库的结构,其中最关键的就是我们将Ultra-PTR封装到了PullToRefreshBaseView基类中,为我们给各种view实现下拉刷新提供了便利的接入。那么今天我们继续给大家呈上PullToRefreshRecyclerView的打造过程,继承PullToRefreshBaseView基类轻松地为RecyclerView实现下拉刷新的功能。
由于之前业务上的需求,PullToRefreshRecyclerView目前只支持LinearLayoutManager的布局方式,也就是说用RecyclerView实现ListView的模式。在后续有时间会考虑接入阿里前段时间开源的VLayout,也能非常轻松的实现各种样式的RecyclerView。
这章节的PullToRefreshRecyclerView,主要实现下拉刷新、上拉加载、数据自动装载刷新、封装统一的adapter、模拟ListView的简单分割线这几项功能。

开始

首先我们继承PullToRefreshBaseView基类创建 一个PullToRefreshRecyclerView,实现onInitContent方法,在其中返回我们要实现的内部容器RecyclerView。

    @Override    public View onInitContent() {        mRecyclerView = new RecyclerView(getContext());        mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(-1, -1));        return mRecyclerView;    }

其次,我们初始化刷新的默认头部、底部,给RecyclerView配置LaytouManager,这样就完成了基本的封装。

    private void initView() {        setDefaultLoadingHeaderView();        setDefaultLoadingFooterView();        setOnRefreshListener(this);        mLinearLayoutManager = new LinearLayoutManager(getContext());        mRecyclerView.setLayoutManager(mLinearLayoutManager);        mRecyclerView.setHasFixedSize(true);    //确定每个item高度相同,提高性能        mRecyclerView.setAdapter(new EmptyRecyclerViewAdapter(getContext()));    }

其中setOnRefreshListener设置的是要实现下拉刷新上拉加载两个监听。
EmptyRecyclerViewAdapter是继承RecyclerView.Adapter实现的一个空的adapter,因为在实际调用过程中,会产生一个警告:

“Recycler View..No adapter attached: skipping layout”

在网上查到的资料中显示,是因为网络请求的数据还没回来就调用了notifyDataSetChanged()导致的,这里只需要加个EmptyRecyclerViewAdapter即可解决。

好啦,写到这里,实际上我们已经实现了RecyclerView下拉刷新、上拉加载的简单封装。现在的PullToRefreshRecyclerView已经具备刷新的功能,通过实现onPullDownToRefresh方法,可以捕获下拉事件;通过实现onPullUpToRefresh方法,可以捕获上拉事件。直接操作mRecyclerView即可实现数据填充、增加list头部底部等。
当然我们的追求远不止那么简单,直接操作mRecyclerView显然不是我们的风格,所以我们进一步对mRecyclerView进行封装处理。

分割线Divider

设置分割线,我们希望能像下面这么简单地调个方法,即可设置item之间间隔的宽度和颜色。

mPullRefreshRecyclerView.setDivider(R.dimen.dp_07, R.color.default_dividing_line);

当然也支持自定义RecyclerView.ItemDecoration。

mPullRefreshRecyclerView.setDivider(mItemDecoration);

首先RecyclerView提供了RecyclerView.ItemDecoration抽象类给我们自定义分割线,实现onDraw方法,利用Canvas绘画即可。

public class PTRRecyclerViewDecoration extends RecyclerView.ItemDecoration {    private Drawable mDivider;    private int dividerHeight;    private int dividerWidth;    private int mOrientation;    public boolean isHadHeader = false;    public boolean isHadFooter = false;    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;    public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable) {        this.mDivider = drawable;        this.dividerHeight = mDivider != null ? mDivider.getIntrinsicHeight() : 0;        this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0;        setOrientation(orientation);    }    public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable, int dividerHeight) {        this.mDivider = drawable;        this.dividerHeight = dividerHeight;        this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0;        setOrientation(orientation);    }    //设置屏幕方向    public void setOrientation(int orientation) {        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {            throw new IllegalArgumentException("invalid orientation");        }        mOrientation = orientation;    }    @Override    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {        if (parent.getChildCount() > 2) {            if (mOrientation == HORIZONTAL_LIST) {                drawVerticalLine(c, parent, state);            } else {                drawHorizontalLine(c, parent, state);            }        }    }    //横向    public void drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state) {        if (parent.getAdapter() == null) {            return;        }        int left = parent.getPaddingLeft();        int right = parent.getWidth() - parent.getPaddingRight();        final int childCount = parent.getChildCount();        int dataEndPosition = parent.getAdapter().getItemCount();        for (int i = 1; i < childCount - 1; i++) {            if (mDivider == null) {                break;            }            View child = parent.getChildAt(i);            int position = parent.getChildAdapterPosition(child);            //获取child的布局信息            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();            final int top = child.getBottom() + params.bottomMargin;            int bottom = top + dividerHeight;            //处理第一个HeaderView、最后一个FooterView分割线            if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) {                bottom = top;            }            mDivider.setBounds(left, top, right, bottom);            mDivider.draw(c);        }    }    //竖向    public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state) {        if (parent.getAdapter() == null) {            return;        }        int top = parent.getPaddingTop();        int bottom = parent.getHeight() - parent.getPaddingBottom();        final int childCount = parent.getChildCount();        int dataEndPosition = parent.getAdapter().getItemCount();        for (int i = 1; i < childCount - 1; i++) {            if (mDivider == null) {                break;            }            View child = parent.getChildAt(i);            int position = parent.getChildAdapterPosition(child);            //获取child的布局信息            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();            final int left = child.getRight() + params.rightMargin;            int right = left + mDivider.getIntrinsicWidth();            //处理第一个HeaderView、最后一个FooterView分割线            if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) {                right = left;            }            mDivider.setBounds(left, top, right, bottom);            mDivider.draw(c);        }    }    @Override    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {        if (mOrientation == HORIZONTAL_LIST) {            outRect.set(0, 0, dividerWidth, 0);        } else {            outRect.set(0, 0, 0, dividerHeight);        }    }}

核心主要还是绘图的代码,这里就不做展开,如果有一些复杂需求的分割线,可以网上搜搜,这方面的资料还是挺多的。
这里主要提一下两个关键的标记变量isHadHeader和isHadFooter。由于我们在下面要加入HeaderView和FooterView的支持,而我们在增加RecyclerView头部和底部的时候,都是不希望加入分割线的。所以这用两个标记变量isHadHeader和isHadFooter来判断,当前是否有加入头部/底部,从而“隐藏”分割线。
有了自定义的PTRRecyclerViewDecoration,我们就实现上面的setDivider方法了。

    public void setDivider(int padding, int divider) {        if (padding > 0 && divider >= 0) {            Drawable _divider = divider != 0 ? getResources().getDrawable(divider) : null;            myDecoration = new PTRRecyclerViewDecoration(getContext(), PTRRecyclerViewDecoration.VERTICAL_LIST, _divider, (int) getResources().getDimension(padding));            mRecyclerView.addItemDecoration(myDecoration);        }    }

优雅地添加HeaderView和FooterView

RcyclerView本身是不提供添加HeaderView和FooterView方法的,需要使用RecyclerView.Adapter来实现。而如果我们直接在我们已经实现的adapter上修改,增加头部和底部,这样我们需要为每个adapter加入同样的代码,显然不符合我们的封装思想。
这一节我们参考了鸿洋大神的下面这篇文章。

Android 优雅的为RecyclerView添加HeaderView和FooterView

采用装饰者模式的思想,给adapter包装一层,专门负责管理头部、底部的添加和删除。

/** * HeaderAndFooterWrapper .java */public class HeaderAndFooterWrapper extends RecyclerView.Adapter{    private static final int BASE_ITEM_TYPE_HEADER = 100000;    private static final int BASE_ITEM_TYPE_FOOTER = 200000;    private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();    private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();    private RecyclerView.Adapter mInnerAdapter;    public HeaderAndFooterWrapper(RecyclerView.Adapter adapter) {        mInnerAdapter = adapter;    }    private boolean isHeaderViewPos(int position) {        return position < getHeadersCount();    }    private boolean isFooterViewPos(int position) {        return position >= getHeadersCount() + getRealItemCount();    }    public void addHeaderView(View view) {        mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);    }    public void addFooterView(View view) {        mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, view);    }    public void addHeaderView(List<View> view) {        for (int i = 0; i < view.size(); i++) {            addHeaderView(view.get(i));        }    }    public void addFooterView(List<View> view) {        for (int i = 0; i < view.size(); i++) {            addFooterView(view.get(i));        }    }    public void removeFooterView(View view) {        int idx = mFooterViews.indexOfValue(view);        if (idx != -1) {            mFooterViews.removeAt(idx);        }    }    public int getHeadersCount() {        return mHeaderViews.size();    }    public int getFootersCount() {        return mFooterViews.size();    }    private int getRealItemCount()    {        return mInnerAdapter.getItemCount();    }    @Override    public int getItemViewType(int position) {        if (isHeaderViewPos(position)) {            return mHeaderViews.keyAt(position);        }else if (isFooterViewPos(position)) {            return mFooterViews.keyAt(position - getHeadersCount() - getRealItemCount());        }        return mInnerAdapter.getItemViewType(position - getHeadersCount());    }    @Override    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        if (mHeaderViews.get(viewType) != null) {            View headerView = mHeaderViews.get(viewType);            headerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));            CommonViewHolder myViewHolder = new CommonViewHolder(headerView);            return myViewHolder;        }else if (mFooterViews.get(viewType) != null) {            View footerView = mFooterViews.get(viewType);            footerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));            CommonViewHolder myViewHolder = new CommonViewHolder(footerView);            return myViewHolder;        }        return mInnerAdapter.onCreateViewHolder(parent, viewType);    }    @Override    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {        if (isHeaderViewPos(position)) {            return;        }else if (isFooterViewPos(position)) {            return;        }        mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());    }    @Override    public int getItemCount() {        return getHeadersCount() + getFootersCount() + getRealItemCount();    }}

有了HeaderAndFooterWrapper这个包装类,我们就可以增加一些方法,来管理PullToRefreshRecyclerView拥有的HeaderView和FooterView。

    private List<View> mHeaderViewList;    private List<View> mFooterViewList;    public void addHeaderView(View headerView) {        if (mHeaderViewList == null){            mHeaderViewList = new ArrayList<>();        }        mHeaderViewList.add(headerView);        //指定分割线标记,当前拥有头部        if(myDecoration != null) {            myDecoration.isHadHeader = true;        }    }    /**     * 增加FooterView     */    public void addFooterView(View footerView) {        if (mFooterViewList == null){            mFooterViewList = new ArrayList<>();        }        mFooterViewList.add(footerView);        //指定分割线标记,当前拥有底部        if(myDecoration != null) {            myDecoration.isHadFooter = true;        }        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();        if (adapter != null && !(adapter instanceof EmptyRecyclerViewAdapter)) {            if (adapter instanceof HeaderAndFooterWrapper) {                ((HeaderAndFooterWrapper) adapter).addFooterView(footerView);                adapter.notifyDataSetChanged();            }else {                HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);                headerAndFooterWrapper.addFooterView(footerView);                mRecyclerView.setAdapter(headerAndFooterWrapper);            }        }    }    /**     * 移除FooterView     */    public void removeFooterView(View footerView) {        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();        if (adapter != null && (adapter instanceof HeaderAndFooterWrapper)) {            ((HeaderAndFooterWrapper) adapter).removeFooterView(footerView);            if (mFooterViewList != null && mFooterViewList.indexOf(footerView) != -1) {                mFooterViewList.remove(footerView);            }        }    }    /**     * 包装adapter,增加HeaderView,FooterView     */    private RecyclerView.Adapter getWrappedListAdapter(RecyclerView.Adapter adapter) {        if ((mHeaderViewList != null && mHeaderViewList.size() != 0) || (mFooterViewList != null && mFooterViewList.size() != 0)) {            HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);            //增加HeaderView            if (mHeaderViewList != null && mHeaderViewList.size() != 0) {                headerAndFooterWrapper.addHeaderView(mHeaderViewList);            }            //增加FooterView            if (mFooterViewList != null && mFooterViewList.size() != 0) {                headerAndFooterWrapper.addFooterView(mFooterViewList);            }            return headerAndFooterWrapper;        }        return adapter;    }

一键式数据装载与Item布局

上一篇文章中,我们提到了OnPullListActionListener接口,提供数据加载、item点击、item初始化、刷新完成的回调方法。主要用于在封装统一adapter的时候,使界面只需要关系接口请求和界面布局,实现一键式的数据装载与item布局指定。

/** * OnPullListActionListener .java */public interface OnPullListActionListener<T> {    void loadData(int pageIndex, String tips);    void clickItem(T item, int position);    void createListItem(ViewHolder holder, T currentItem, List<T> list, int position);    void onRefreshComplete();}
  • loadData:发起获取数据请求
  • clickItem:item点击事件
  • createListItem:初始化item布局,其中ViewHolder是统一View控制器,避免要定义一系列view的变量;currentItem是当前item的数据
  • onRefreshComplete:加载完成事件

/** * ViewHolder.java */public class ViewHolder {    private final SparseArray<View> mViews;    private View mConvertView;    private OnClickListener mOnClickListener;    public ViewHolder(View parent) {        mConvertView = parent;        mViews = new SparseArray<View>();    }    public ViewHolder(View parent, OnClickListener clickListener) {        mOnClickListener = clickListener;        mConvertView = parent;        mViews = new SparseArray<View>();    }    private ViewHolder(Context context, ViewGroup parent, int layoutId) {        this.mViews = new SparseArray<View>();        mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);        mConvertView.setTag(this);    }    public static ViewHolder get(Context context, View convertView,                                 ViewGroup parent, int layoutId, int position) {        if (convertView == null) {            return new ViewHolder(context, parent, layoutId);        }        return (ViewHolder) convertView.getTag();    }    public View getConvertView() {        return mConvertView;    }    public <T extends View> T getView(int viewId) {        View view = mViews.get(viewId);        if (view == null) {            view = mConvertView.findViewById(viewId);            mViews.put(viewId, view);        }        return (T) view;    }    public View setOnClickListener(int viewId) {        View view = getView(viewId);        setClickListener(view);        return view;    }    public TextView setText(int viewId, CharSequence text) {        TextView view = getView(viewId);        if (view != null) {            view.setText(text);        }        return view;    }    public TextView setTextColor(int viewId, int Color) {        TextView view = getView(viewId);        if (view != null) {            view.setTextColor(Color);        }        return view;    }    public ImageView setImageResource(int viewId, int drawableId) {        ImageView view = getView(viewId);        if (view != null) {            view.setImageResource(drawableId);        }        return view;    }    public View setBackgroundResource(int viewId, int drawableId) {        View view = getView(viewId);        if (view != null) {            view.setBackgroundResource(drawableId);        }        return view;    }    public View setVisibility(int viewId, int visibility) {        View view = getView(viewId);        view.setVisibility(visibility);        return view;    }    public int getVisibility(int viewId) {        View view = getView(viewId);        return view.getVisibility();    }    public void setClickListener(View view) {        if (mOnClickListener != null && view != null) {            view.setOnClickListener(mOnClickListener);        }    }    public void setClickListener(OnClickListener clickListener) {        mOnClickListener = clickListener;    }}

我们有了ViewHolder对View的统一控制,就可以在adapter中使用起来,继承RecyclerView.Adapter我们可以创建一个公共的CommonBaseAdapter,封装CommonViewHolder用来包装上面的ViewHolder,即可实现一个通用的Adapter,而不需要自己每次单独实现RecyclerView.ViewHolder。

/** * CommonViewHolder.java * 这部分代码非常简单,就封装了一个上面的ViewHolder */public class CommonViewHolder extends RecyclerView.ViewHolder {    public CommonViewHolder(View view) {        super(view);    }    ViewHolder viewHolder;}

公共的CommonBaseAdapter。

/** * CommonBaseAdapter.java */public abstract class CommonBaseAdapter<T> extends RecyclerView.Adapter<CommonViewHolder>{    private Context mContext;    private List<T> mData;    protected final int mItemLayoutId;    public CommonBaseAdapter(Context context, List<T> mData, int itemLayoutId)    {        this.mContext = context;        this.mItemLayoutId = itemLayoutId;        this.mData = mData;    }    @Override    public CommonViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        ViewHolder viewHolder = getViewHolder(0, null, parent);        CommonViewHolder myViewHolder = new CommonViewHolder(viewHolder.getConvertView());        myViewHolder.viewHolder = viewHolder;        return myViewHolder;    }    @Override    public void onBindViewHolder(final CommonViewHolder holder, final int position) {        holder.itemView.setOnClickListener(new View.OnClickListener()        {            @Override            public void onClick(View v)            {                onItemClick(holder.itemView, position);            }        });        convert(holder.viewHolder, mData.get(position), mData, position);    }    private ViewHolder getViewHolder(int position, View convertView, ViewGroup parent) {        return ViewHolder.get(mContext, convertView, parent, mItemLayoutId, position);    }    protected abstract void convert(ViewHolder holder, T item,List<T> list,int position);    protected abstract void onItemClick(View itemView, int position);    @Override    public int getItemCount() {        return mData.size();    }}

这样,我们可以在PullToRefreshRecyclerView创建一个内部类MyListAdapter,用来实现CommonBaseAdapter,从而将adapter内的创建、点击事件通过OnPullListActionListener传递到上层。

    private class MyListAdapter extends CommonBaseAdapter<T> {        public MyListAdapter(Context context, List<T> mData, int itemLayoutId) {            super(context, mData, itemLayoutId);        }        @Override        protected void onItemClick(View itemView, int position) {            if (position >= 0 && mList.size() > 0) {                T item = mList.get(position);                if (mOnPullListActionListener != null && item != null) {                    int numHeaderView = mHeaderViewList != null ? mHeaderViewList.size() : 0;                    mOnPullListActionListener.clickItem(item, position + numHeaderView);                }            }        }        @Override        protected void convert(ViewHolder holder, T item, List<T> list, int position) {            if (mOnPullListActionListener != null && item != null) {                mOnPullListActionListener.createListItem(holder, item, list, position);            }        }    }

这里,我们先简单回顾一下,看看我们上面都实现了哪些功能。

  • 添加分割线:setDivider(int padding, int divider)
  • 添加HeaderView:addHeaderView(View headerView),
  • 添加FooerView:addFooterView(View footerView)
  • 移除FooterView:removeFooterView(View footerView)
  • 初始化Item布局:通过接口OnPullListActionListener的createListItem,可以拿到ViewHolder,轻松实现布局和填充item的数据
  • Item点击:同样通过接口OnPullListActionListener的clickItem,可以捕获Item点击事件

一切似乎都已经非常强大了,但貌似还漏了些什么。没错万事具备,只欠东风,我们还缺少了最关键的加载数据,和数据展示。不急,马上为您呈上!
OnPullListActionListener接口还有一个关键的loadData()方法,这当然就是用来为我们加载数据调用的。

    /**     * 下拉刷新加载数据     */    public void loadRefreshData(boolean isShowTops) {        String tips = isShowTops ? TIPS_LOAD_DATA : "";        mPageIndex = 1;        if (mOnPullListActionListener != null) {            mOnPullListActionListener.loadData(mPageIndex, tips);        }    }    /**     * 上拉刷新加载更多数据     */    public void loadMoreData(int taskId, boolean isShowTops) {        String tips = isShowTops ? TIPS_LOAD_DATA : "";        if (mOnPullListActionListener != null) {            mOnPullListActionListener.loadData(mPageIndex, tips);        }    }

在上面我们已经实现了对adapter的一个包装类MyListAdapter,因此这里可以很简单的实现数据的装载与刷新。

    /**     * 显示数据     * 传入数据数组list,和指定的item布局itemLayoutId     */    public void showAllData(List<T> list, int itemLayoutId) {        if (commonBaseAdapter == null) {            commonBaseAdapter = new MyListAdapter(getContext(), list, itemLayoutId);            mRecyclerView.setAdapter(getWrappedListAdapter(commonBaseAdapter));        } else {            getAdapter().notifyDataSetChanged();        }    }

写在最后

到这里,我们就将PullToRefreshRecyclerView封装RecyclerView的打造过程,完整地分析给了大家。完整的源码这里就不再贴出来了,我们在上一篇已经贴给大家了。
其实封装PullToRefreshRecyclerView的时候,更多是从我们项目的需求出发,所以我们暂时只实现了LinearLayoutManager列表式布局,在后面如果有时间,我打算接入阿里的VLayout,这样就能实现各种样式的下拉刷新RecyclerView。这里大家如果有兴趣,也可以继承PullToRefreshBaseView尝试自己实现一下,欢迎一起交流,共同学习!

阅读全文
0 0