细谈RecyclerView:(二)刷新闪烁?不存在的,带你了解RecyclerView局部刷新
在上一篇文章中我们谈到了如何利用RecyclerView来优化布局,使用的方式的是通过多布局的方式来实现的。如果你还没有看过之前的文章,记得去看一下哦。细谈RecyclerView:(一)优化布局
在第一篇文章中提到到,如果想使用RecyclerView,那么肯定是需要要创建想对应的Adapter。
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return null; } public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { } public int getItemCount() { return 0; }
每个方法的作用是什么,想必就需要我多提了。今天我们要谈论的话题是:局部刷新。
需求场景
通过比较两张图片我们可以发现,这个是要涉及到对单本书的刷新的。默认状态下是不会显示选中的那个图标的,当点击右上角编辑的按钮后,所有的书籍都会显示一个可以选择的图标。
现在我们来分析这个需求。比较两次不同的状态下书籍的布局,我们不难发现:输的封面是一直不变的。更准确的说除了在编辑状态下多显示了一个选择图标外其他的东西都没有发生变化。
通过上面的分析,我们可能会想到:能不能在刷新单个布局(我习惯称为Item)的时候只对选择图标的显示和隐藏做更新?看完今天的这篇文章后,你就知道是可以的。
讲解
首先我们需要注意一个方法:
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { }
这是我们在覆写Adapter方法中需要覆写的一个方法。
在RecyclerView.Adapter的方法中,官方的注释是这样的。
/** * Called by RecyclerView to display the data at the specified position. This method should update the contents of the itemView to reflect the item at the given position. * Note that unlike ListView, RecyclerView will not call this method * again if the position of the item changes in the data set unless the item itself is * invalidated or the new position cannot be determined. For this reason, you should only * use the <code>position</code> parameter while acquiring the related data item inside * this method and should not keep a copy of it. If you need the position of an item later * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will * have the updated adapter position. * * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can * handle efficient partial bind. * * @param holder The ViewHolder which should be updated to represent the contents of the * item at the given position in the data set. * @param position The position of the item within the adapter's data set. */ public abstract void onBindViewHolder(VH holder, int position);
大概的意思是:这个方法被调用的目的是为了显示指定位置上的数据。这个方法会通过给定的位置进行刷新Item的内容。
其实在RecyclerView.Adapter中还存在着另外一个重载的方法。
public void onBindViewHolder(VH holder, int position, List<Object> payloads) { onBindViewHolder(holder, position); }
与含有两个参数的方法相比,这个方法中多了一个参数:List<Object> payloads。
根据英文的翻译来讲,payloads的意思是“有效载荷”的意思。那么“有效载荷”又是啥意思呢?
有效载荷是指航天器上装载的为直接实现航天器在轨运行要完成的特定任务的仪器、设备、人员、试验生物及试件等。航天器有效载荷是航天器在轨发挥最终航天使命的最重要的一个分系统。
好吧,其实我也没懂。那么我们在搜一下“载荷”是啥意思。
荷载指的是使结构或构件产生内力和变形的外力及其它因素。
这个好像明白点了。
好吧,那我们再看看官方是如何解释的。
The payloads parameter is a merge list from { #notifyItemChanged(int, Object)} or * { #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty, * the ViewHolder is currently bound to old data and Adapter may run an efficient partial * update using the payload info. If the payload is empty, Adapter must run a full bind. * Adapter should not assume that the payload passed in notify methods will be received by * onBindViewHolder(). For example when the view is not attached to the screen, the * payload in notifyItemChange() will be simply dropped.
首先payLoads是一个List<Object>的集合,它来源于两个方法中的参数:一个是notifyItemChanged(int, Object),另外一个是notifyItemRangeChanged(int, int, Object)。如果payloads不为空,那么ViewHolder只会更新payloads中传过来的信息,也就是进行局部的更新(run an efficient partial update),如果为空,那么Adapter需要进行一个全新的绑定。
通过以上的解释,我们大概明白了:对ViewHolder(Item)进行局部刷新的关键其实是payloads参数,而如果想实现局部刷新,那么payloads肯定不能为空,而且还需要调用notifyItemChanged(int, Object)或者是notifyItemRangeChanged(int, int, Object)的方法以实现局部刷新。
那么现在我们利用payloads来实现需求场景中的功能。
public void onBindViewHolder(BookshelfViewHolder holder, int position, List<Object> payloads) { //当payloads为空时,不需要刷新的控件 if (payloads.isEmpty()) { BookShelfBook book = mBookShelfDataList.get(position); ImageLoader.loadBookCover(holder.bookCoverIv,book.imageUrl); holder.nameTv.setText(book.name); if(book.isRead == Flag.FALSE){ holder.unReadTv.setVisibility(View.VISIBLE); holder.updateTv.setVisibility(View.GONE); }else { if (book.updateCnt > 0) { holder.updateTv.setVisibility(View.VISIBLE); holder.unReadTv.setVisibility(View.GONE); String updateCount = mContext.getText(R.string.update_chapter_count).toString(); holder.updateTv.setText(String.format(updateCount,book.updateCnt)); }else { holder.updateTv.setVisibility(View.GONE); holder.unReadTv.setVisibility(View.VISIBLE); } } holder.layout.setOnClickListener(new NoDoubleClickListener() { protected void onNoDoubleClick(View view) { if (isEdit) { if (book.isSelected) { book.isSelected = false; holder.chooseIv.setSelected(false); mSelectedList.remove(book); if (mSelectedList.size() <= 0) { ((BookShelfFragment)mFragment).showBottomBar(false); } }else { book.isSelected = true; holder.chooseIv.setSelected(true); mSelectedList.add(book); if (mSelectedList.size() > 0) { ((BookShelfFragment)mFragment).showBottomBar(true); } } ((BookShelfFragment)mFragment).setBottomState(mSelectedList.size()); }else { BookDetailActivity.actionStart(mContext,book.bookUuid); } } }); }else { //由于项目中只需要控制选择框的显示和隐藏所以只要判断payloads不为空就行,当然如果你需要实现很多个不同的控件刷新的时候那你还需要进一步判断payloads以做到更精准的局部刷新 if (isEdit) { holder.chooseIv.setVisibility(View.VISIBLE); }else { holder.chooseIv.setVisibility(View.GONE); } if (isAll) { holder.chooseIv.setSelected(true); mSelectedList.clear(); mSelectedList.addAll(mBookShelfDataList); }else { holder.chooseIv.setSelected(false); mSelectedList.clear(); } } }
解释都写在代码上了,我就不展开讲了。总之我们的思路是:当需要控制某个ViewHolder控件更新的时候就再更新的时候传入一个payloads。由于payloads是一个List<Object>所以你可以控制多个控件的刷新。有一点需要注意的是你在调用Adapter的更新方法的时候一定要调用含有List<Object>的方法,要么是不会奏效的。这个在官方的方法注释中也提到了。
为啥要写这篇文章
如果使用两次参数的方法没有问题的话,我想我也不会去用含三个参数的方法,主要是在开发的过程中遇到了一个刷新闪烁的问题。大概的意思就是当我点击某个Item的时候,另外一个Item的图书封面会消失,再点击然后又会显示。所以就想着能不能做到局部刷新,毕竟图书的封面在默认状态下和编辑的状态下是不会发生改变的。所以就Google了一下,发现是可以的。所以在实现项目功能的时候也希望自己能够在以后的开发中能够多使用局部刷新以减少不必要的更新。
最后
非常感谢您的阅读,如果文章中有错误或值得商榷的地方还希望您能够在评论区指出。