細談RecyclerView:(二)重新整理閃爍?不存在的,帶你瞭解RecyclerView區域性重新整理

零下0814發表於2018-03-09

在上一篇文章中我們談到了如何利用RecyclerView來優化佈局,使用的方式的是通過多佈局的方式來實現的。如果你還沒有看過之前的文章,記得去看一下哦。細談RecyclerView:(一)優化佈局

在第一篇文章中提到到,如果想使用RecyclerView,那麼肯定是需要要建立想對應的Adapter。

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

    }

    @Override
    public int getItemCount() {
        return 0;
    }

每個方法的作用是什麼,想必就需要我多提了。今天我們要談論的話題是:區域性重新整理。

需求場景


這是一個書架的預設介面。


這個是編輯狀態下的介面。

通過比較兩張圖片我們可以發現,這個是要涉及到對單本書的重新整理的。預設狀態下是不會顯示選中的那個圖示的,當點選右上角編輯的按鈕後,所有的書籍都會顯示一個可以選擇的圖示。

現在我們來分析這個需求。比較兩次不同的狀態下書籍的佈局,我們不難發現:輸的封面是一直不變的。更準確的說除了在編輯狀態下多顯示了一個選擇圖示外其他的東西都沒有發生變化。

通過上面的分析,我們可能會想到:能不能在重新整理單個佈局(我習慣稱為Item)的時候只對選擇圖示的顯示和隱藏做更新?看完今天的這篇文章後,你就知道是可以的。

講解

首先我們需要注意一個方法:


@Override
    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 {@link #notifyItemChanged(int, Object)} or
         * {@link #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來實現需求場景中的功能。


@Override
    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() {
                @Override
                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了一下,發現是可以的。所以在實現專案功能的時候也希望自己能夠在以後的開發中能夠多使用區域性重新整理以減少不必要的更新。

最後

非常感謝您的閱讀,如果文章中有錯誤或值得商榷的地方還希望您能夠在評論區指出。

相關文章