Android 实现自己的RecyclerView加载更多

fuvk8931 8年前
   <p>很多时候,项目中都会有列表加载更多的场景,这次我们让RecyclerView轻松拥有加载更多的功能。虽然已有许多类似的轮子,但有的功能过于复杂,其实很多都用不到,所以不妨打造更适合自己的轮子。</p>    <p>我们的RecyclerView加载更多是通过其Adapter子类实现的,接下来我们一步步的构建Adapter吧!</p>    <h2><strong>1、编写通用的Adapter、ViewHolder</strong></h2>    <p>一般情况下使用Adapter都要为其创建一个ViewHolder,既然要编写通用的Adapter,首先要有一个通用的ViewHolder:</p>    <pre>  <code class="language-java">public class ViewHolder extends RecyclerView.ViewHolder {      private SparseArray<View> mViews;      private View mConvertView;        private ViewHolder(View itemView) {          super(itemView);          mConvertView = itemView;          mViews = new SparseArray<>();      }        public static ViewHolder create(Context context, int layoutId, ViewGroup parent) {          View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);          return new ViewHolder(itemView);      }        public static ViewHolder create(View itemView) {          return new ViewHolder(itemView);      }        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 getConvertView() {          return mConvertView;      }        public void setText(int viewId, String text) {          TextView textView = getView(viewId);          textView.setText(text);      }      .......省略其它辅助方法.........  }</code></pre>    <p>我们自定义的ViewHolder类可以根据布局文件的id或具体的itemView返回一个ViewHolder对象,并用SparseArray来缓存我们itemView中的子View,避免每次都要去解析子View,同时提供相关辅助方法设置itemView的内容。有了ViewHolder,接下来编写Adapter就简单了:</p>    <pre>  <code class="language-java">public abstract class BaseAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {      public static final int TYPE_COMMON_VIEW = 100001;        private OnItemClickListeners<T> mItemClickListener;        protected Context mContext;      protected List<T> mDatas;        protected abstract void convert(ViewHolder holder, T data);        protected abstract int getItemLayoutId();        public BaseAdapter(Context context, List<T> datas) {          mContext = context;          mDatas = datas == null ? new ArrayList<T>() : datas;      }        @Override      public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {          ViewHolder viewHolder = null;          switch (viewType) {              case TYPE_COMMON_VIEW:                  viewHolder = ViewHolder.create(mContext, getItemLayoutId(), parent);                  break;          }          return viewHolder;      }        @Override      public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {          switch (holder.getItemViewType()) {              case TYPE_COMMON_VIEW:                  bindCommonItem(holder, position);                  break;          }      }        private void bindCommonItem(RecyclerView.ViewHolder holder, final int position) {          final ViewHolder viewHolder = (ViewHolder) holder;          convert(viewHolder, mDatas.get(position));          viewHolder.getConvertView().setOnClickListener(new View.OnClickListener() {              @Override              public void onClick(View view) {                  mItemClickListener.onItemClick(viewHolder, mDatas.get(position), position);              }          });      }        @Override      public int getItemCount() {          return mDatas.size();      }        @Override      public int getItemViewType(int position) {          return TYPE_COMMON_VIEW;      }        public T getItem(int position) {          if (mDatas.isEmpty()) {              return null;          }          return mDatas.get(position);      }        public void setOnItemClickListener(OnItemClickListeners<T> itemClickListener) {          mItemClickListener = itemClickListener;      }  }</code></pre>    <p>很简单,继承RecyclerView.Adapter,重写相关方法,提供了 getItemLayoutId() 、 convert() 两个抽象方法供BaseAdapter的子类实现,来初始化item的布局id,以及item内容,同时通过 OnItemClickListeners 接口为item绑定点击事件。</p>    <p>编写好了Adapter,我们在其构造方法中添加一个参数 isOpenLoadMore ,来表示是否开启加载更多:</p>    <pre>  <code class="language-java">public BaseAdapter(Context context, List<T> datas, boolean isOpenLoadMore) {          mContext = context;          mDatas = datas == null ? new ArrayList<T>() : datas;          mOpenLoadMore = isOpenLoadMore;      }</code></pre>    <p>这样初级版本的Adapter就完成了。</p>    <h2><strong>2、添加Footer View</strong></h2>    <p>接下来就要添加Footer View,这样才能有加载更多的视觉效果么。其实很简单,如果当前item的position满足如下条件:</p>    <pre>  <code class="language-java">private boolean isFooterView(int position) {          return mOpenLoadMore && position >= getItemCount() - 1;      }</code></pre>    <p>即已经开启加载更多、当前position在列表的尾部,则在 getItemViewType() 返回</p>    <pre>  <code class="language-java">@Override      public int getItemViewType(int position) {          if (isFooterView(position)) {              return TYPE_FOOTER_VIEW;          }      }</code></pre>    <p>之后会创建Footer View对应的ViewHolder:</p>    <pre>  <code class="language-java">@Override      public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {          ViewHolder viewHolder = null;          switch (viewType) {              case TYPE_FOOTER_VIEW:                  if (mFooterLayout == null) {                      mFooterLayout = new RelativeLayout(mContext);                  }                  viewHolder = ViewHolder.create(mFooterLayout);                  break;          }          return viewHolder;      }</code></pre>    <p>可以看到 mFooterLayout 是一个空的Container,因为要根据加载更多对应的状态来更新 mFooterLayout ,这个稍后再说。</p>    <p>这样Footer View就添加完了吗?当然没有,我们需要针对StaggeredGridLayoutManager、GridLayoutManager模式分别重写 onViewAttachedToWindow() 、 onAttachedToRecyclerView() 方法,否则会出现Footer View不能在列表底部占据一行的问题:</p>    <pre>  <code class="language-java">@Override      public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {          super.onViewAttachedToWindow(holder);          if (isFooterView(holder.getLayoutPosition())) {              ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();                if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {                  StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;                  p.setFullSpan(true);              }          }      }        @Override      public void onAttachedToRecyclerView(RecyclerView recyclerView) {          super.onAttachedToRecyclerView(recyclerView);          final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();          if (layoutManager instanceof GridLayoutManager) {              final GridLayoutManager gridManager = ((GridLayoutManager) layoutManager);              gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {                  @Override                  public int getSpanSize(int position) {                      if (isFooterView(position)) {                          return gridManager.getSpanCount();                      }                      return 1;                  }              });          }      }</code></pre>    <p>到此无论是那种形式的列表都能正常添加Footer View了。</p>    <h2><strong>3、判断列表是否滚动到了底部</strong></h2>    <p>按照常理,只有滑动到列表的底部才会触发加载更多的操作,之前提到了 onAttachedToRecyclerView() 方法,通过该方法可以得到Adapter所绑定的RecyclerView,这样就能监听RecyclerView的滚动事件,进而判断列表是否滚动了底部:</p>    <pre>  <code class="language-java">private void startLoadMore(RecyclerView recyclerView, final RecyclerView.LayoutManager layoutManager) {          if (!mOpenLoadMore || mLoadMoreListener == null) {              return;          }            recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {              @Override              public void onScrollStateChanged(RecyclerView recyclerView, int newState) {                  super.onScrollStateChanged(recyclerView, newState);                  if (newState == RecyclerView.SCROLL_STATE_IDLE) {                      if (!isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {                          scrollLoadMore();                      }                  }              }                @Override              public void onScrolled(RecyclerView recyclerView, int dx, int dy) {                  super.onScrolled(recyclerView, dx, dy);                  if (isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {                      scrollLoadMore();                  } else if (isAutoLoadMore) {                      isAutoLoadMore = false;                  }              }          });      }</code></pre>    <p>我们单独封装了 startLoadMore() 方法,当列表滚动状态改变会回调 onScrollStateChanged() 方法,如果状态为 <strong>SCROLL_STATE_IDLE</strong> ,并且当前可见的item位置为列表最后一项,则开始加载更多数据。这里还重写了 onScrolled() 方法,当列表滚动结束后会回调,重写该方法有什么用呢?如果初始item不满一屏幕,则可在该方法中加载更多数据,直到item占满一屏幕,也就自动加载更多。我们用 isAutoLoadMore 来区分这种情况,如果 isAutoLoadMore 为true,则Footer View可见则自动加载更多。</p>    <p>再看一下 scrollLoadMore() 方法:</p>    <pre>  <code class="language-java">private void scrollLoadMore() {          if (mFooterLayout.getChildAt(0) == mLoadingView) {              mLoadMoreListener.onLoadMore(false);          }      }</code></pre>    <p>如果当前的Footer View 是正在加载的状态,则调用 OnLoadMoreListener 接口的 onLoadMore() 方法进行具体的加载操作,该方法有一个boolean类型的参数,表示是否重新加载,因为存在加载失败的情况,这样可方便使用。</p>    <h2><strong>4、更新Footer View布局样式</strong></h2>    <p>到这里,我们已经明确了加载更多操作的触发时机,接下来就是在加载更多的时候来更新Footer View,我们定义了三种状态: <strong>加载中、加载失败、加载结束</strong> ,通过如下方法将对应状态的View或布局id添加到Footer View中:</p>    <pre>  <code class="language-java">public void setLoadingView(int loadingId) {          setLoadingView(Util.inflate(mContext, loadingId));      }    public void setLoadFailedView(int loadFailedId) {          setLoadFailedView(Util.inflate(mContext, loadFailedId));      }    public void setLoadEndView(int loadEndId) {          setLoadEndView(Util.inflate(mContext, loadEndId));      }</code></pre>    <p>这三个方法时是通过布局id来给Footer View设置新样式,当然还有通过View来设置的重载方法。在初始化Adapter时可以调用 setLoadingView() 来设置加载中的Footer View样式,如果加载失败了可调用 setLoadFailedView() 、如果加载结束没有更多数据则可以调用 setLoadEndView() 设对应的布局样式。其实就是先移除 mFooterLayout 的子View,然后将新的布局添加进去。</p>    <h2><strong>5、添加EmptyView</strong></h2>    <p>考虑一种情况,如果初始化时,需要先从网络请求数据,然后再更新列表,则一般需要有一个加载提示,所以我们有必要将这个小功能也封装到Adapter中,这样就省去了修改界面布局或者手动显示、隐藏加载提示的步骤。</p>    <p>实现也很简单,先看如下代码:</p>    <pre>  <code class="language-java">@Override      public int getItemCount() {          if (mDatas.isEmpty() && mEmptyView != null) {              return 1;          }      }</code></pre>    <p>如果mData为空,且设置了EmptyView则 getItemCount() 直接返回1。同理返回的item类型为 <strong>TYPE_EMPTY_VIEW</strong> ,代表EmptyView:</p>    <pre>  <code class="language-java">@Override      public int getItemViewType(int position) {          if (mDatas.isEmpty() && mEmptyView != null) {              return TYPE_EMPTY_VIEW;          }      }</code></pre>    <p>在 onCreateViewHolder() 方法中会创建对应的ViewHolder。</p>    <pre>  <code class="language-java">@Override      public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {          ViewHolder viewHolder = null;          switch (viewType) {              case TYPE_EMPTY_VIEW:                  viewHolder = ViewHolder.create(mEmptyView);                  break;          }          return viewHolder;      }</code></pre>    <p>同时提供方法在初始化Adapter时设置EmptyView:</p>    <pre>  <code class="language-java">public void setEmptyView(View emptyView) {          mEmptyView = emptyView;      }</code></pre>    <h2><strong>6、具体使用</strong></h2>    <p>完成了封装,来看看具体的使用,首先创建一个 RefreshAdapter 继承我们的BaseAdapter:</p>    <pre>  <code class="language-java">public class RefreshAdapter extends BaseAdapter<String> {        public RefreshAdapter(Context context, List<String> datas, boolean isLoadMore) {          super(context, datas, isLoadMore);      }        @Override      protected void convert(ViewHolder holder, final String data) {          holder.setText(R.id.item_title, data);          holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {              @Override              public void onClick(View view) {                  Toast.makeText(mContext, "我是" + data + "的button", Toast.LENGTH_SHORT).show();              }          });      }        @Override      protected int getItemLayoutId() {          return R.layout.item_layout;      }  }</code></pre>    <p>在 getItemLayoutId() 中返回item布局id,在 convert() 中初始化item的内容。有了RefreshAdapter,接下来看Activity的操作:</p>    <pre>  <code class="language-java">@Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);            //初始化adapter          mAdapter = new RefreshAdapter(this, null, true);            //初始化EmptyView          View emptyView = LayoutInflater.from(this).inflate(R.layout.empty_layout, (ViewGroup) mRecyclerView.getParent(), false);          mAdapter.setEmptyView(emptyView);            //初始化 开始加载更多的loading View          mAdapter.setLoadingView(R.layout.load_loading_layout);            //设置加载更多触发的事件监听          mAdapter.setOnLoadMoreListener(new OnLoadMoreListener() {              @Override              public void onLoadMore(boolean isReload) {                  loadMore();              }          });            //设置item点击事件监听          mAdapter.setOnItemClickListener(new OnItemClickListeners<String>() {                @Override              public void onItemClick(ViewHolder viewHolder, String data, int position) {                  Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();              }          });            LinearLayoutManager layoutManager = new LinearLayoutManager(this);          layoutManager.setOrientation(LinearLayoutManager.VERTICAL);          mRecyclerView.setLayoutManager(layoutManager);            mRecyclerView.setAdapter(mAdapter);              //延时3s刷新列表          new Handler().postDelayed(new Runnable() {              @Override              public void run() {                  List<String> data = new ArrayList<>();                  for (int i = 0; i < 12; i++) {                      data.add("item--" + i);                  }                  //刷新数据                  mAdapter.setNewData(data);              }          }, 3000);      }</code></pre>    <p>注释已经很详细了,就不多说了。其中 loadMore() 方法如下:</p>    <pre>  <code class="language-java">private void loadMore() {            new Handler().postDelayed(new Runnable() {              @Override              public void run() {                    if (mAdapter.getItemCount() > 15 && isFailed) {                      isFailed = false;                      //加载失败,更新footer view提示                      mAdapter.setLoadFailedView(R.layout.load_failed_layout);                  } else if (mAdapter.getItemCount() > 17) {                      //加载完成,更新footer view提示                      mAdapter.setLoadEndView(R.layout.load_end_layout);                  } else {                      final List<String> data = new ArrayList<>();                      for (int i = 0; i < 2; i++) {                          data.add("item--" + (mAdapter.getItemCount() + i - 1));                      }                      //刷新数据                      mAdapter.setLoadMoreData(data);                  }              }          }, 2000);      }</code></pre>    <p>就是延时2s更新列表数据,同时人为模拟加载失败和结束的情况。</p>    <h2><strong>7、效果</strong></h2>    <p>运行后,看具体的效果:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/62af42736ed4e598f27490f77c24630e.gif"></p>    <p style="text-align: center;">EmptyView</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/dba8188e58968456ad5c4a3d0c2caab8.gif"></p>    <p style="text-align: center;">loading</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/3f41f654f58d42893f6a76edf12b8eb6.gif"></p>    <p style="text-align: center;">load_failed</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/5fcf35a6693c9f7bb50ca14e1e78c61f.gif"></p>    <p style="text-align: center;">load_end</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/25f74f3ac35df217223c545c136920ee.gif"></p>    <p style="text-align: center;">auto_load</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/66c065874848</p>    <p> </p>