RecyclerView問題彙總

楊充發表於2019-05-05

目錄介紹

  • 25.0.0.0 請說一下RecyclerView?adapter的作用是什麼,幾個方法是做什麼用的?如何理解adapter訂閱者模式?

  • 25.0.0.1 ViewHolder的作用是什麼?如何理解ViewHolder的複用?什麼時候停止呼叫onCreateViewHolder?

  • 25.0.0.2 ViewHolder封裝如何對findViewById優化?ViewHolder中為何使用SparseArray替代HashMap儲存viewId?

  • 25.0.0.3 LayoutManager作用是什麼?LayoutManager樣式有哪些?setLayoutManager原始碼裡做了什麼?

  • 25.0.0.4 SnapHelper主要是做什麼用的?SnapHelper是怎麼實現支援RecyclerView的對齊方式?

  • 25.0.0.5 SpanSizeLookup的作用是幹什麼的?SpanSizeLookup如何使用?SpanSizeLookup實現原理如何理解?

  • 25.0.0.6 ItemDecoration的用途是什麼?自定義ItemDecoration有哪些重寫方法?分析一下addItemDecoration()原始碼?

  • 25.0.0.7 上拉載入更多的功能是如何做的?新增滾動監聽事件需要注意什麼問題?網格佈局上拉載入如何優化?

  • 25.0.0.8 RecyclerView繪製原理如何理解?效能優化本質是什麼?RecyclerView繪製原理過程大概是怎樣的?

  • 25.0.0.9 RecyclerView的Recyler是如何實現ViewHolder的快取?如何理解recyclerView三級快取是如何實現的?

  • 25.0.1.0 螢幕滑動(狀態是item狀態可見,不可見,即將可見變化)時三級快取是如何理解的?adapter中的幾個方法是如何變化?

  • 25.0.1.1 SnapHelper有哪些重要的方法,其作用就是是什麼?LinearSnapHelper中是如何實現滾動停止的?

  • 25.0.1.2 LinearSnapHelper程式碼中calculateDistanceToFinalSnap作用是什麼?那麼out[0]和out[1]分別指什麼?

  • 25.0.1.3 如何實現可以設定分割線的顏色,寬度,以及到左右兩邊的寬度間距的自定義分割線,說一下思路?

  • 25.0.1.4 如何實現複雜type首頁需求?如果不封裝會出現什麼問題和弊端?如何提高程式碼的簡便性和高效性?

  • 25.0.1.5 關於item條目點選事件在onCreateViewHolder中寫和在onBindViewHolder中寫有何區別?如何優化?

  • 25.0.1.6 RecyclerView滑動卡頓原因有哪些?如何解決巢狀佈局滑動衝突?如何解決RecyclerView實現畫廊卡頓?

  • 25.0.1.7 RecyclerView常見的優化有哪些?實際開發中都是怎麼做的,優化前後對比效能上有何提升?

  • 25.0.1.8 如何解決RecyclerView巢狀RecyclerView條目自動上滾的Bug?如何解決ScrollView巢狀RecyclerView滑動衝突?

  • 25.0.1.9 如何處理ViewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager?如何解決RecyclerView使用Glide載入圖片導致圖片錯亂問題?

  • 00.RecyclerView複雜封裝庫

    • 幾乎融合了該系列部落格中絕大部分的知識點,歡迎一遍看部落格一遍實踐,一步步從簡單實現功能強大的庫
  • 01.RecyclerView

    • RecycleView的結構,RecyclerView簡單用法介紹
  • 02.Adapter

    • RecyclerView.Adapter扮演的角色,一般常用的重寫方法說明,資料變更通知之觀察者模式,檢視.notifyChanged();原始碼
  • 03.ViewHolder

    • ViewHolder的作用,如何理解對於ViewHolder物件的數量“夠用”之後就停止呼叫onCreateViewHolder方法,ViewHolder簡單封裝
  • 04.LayoutManager

    • LayoutManager作用是什麼?setLayoutManager原始碼分析
  • 05.SnapHelper

    • SnapHelper作用,什麼是Fling操作 ,SnapHelper類重要的方法,
  • 06.ItemTouchHelper

  • 07.SpanSizeLookup

    • SpanSizeLookup如何使用,同時包含列表,2列的網格,3列的網格如何優雅實現?
  • 08.ItemDecoration

    • ItemDecoration的用途,addItemDecoration()原始碼分析
  • 09.RecycledViewPool

    • RecyclerViewPool用於多個RecyclerView之間共享View。
  • 10.ItemAnimator

    • 官方有一個預設Item動畫類DafaultItemAnimator,其中DefaultItemAnimator繼承了SimpleItemAnimator,在繼承了RecyclerView.ItemAnimator,它是如何實現動畫呢?
  • 11.RecyclerView上拉載入

    • 新增recyclerView的滑動事件,上拉載入分頁資料,設定上拉載入的底部footer佈局,顯示和隱藏footer佈局
  • 12.RecyclerView快取原理

    • RecyclerView做效能優化要說複雜也複雜,比如說佈局優化,快取,預載入,複用池,重新整理資料等等
  • 13.SnapHelper原始碼分析

    • SnapHelper旨在支援RecyclerView的對齊方式,也就是通過計算對齊RecyclerView中TargetView 的指定點或者容器中的任何畫素點。
  • 16.自定義SnapHelper

    • 自定義SnapHelper
  • 18.ItemTouchHelper 實現互動動畫

    • 需要自定義類實現ItemTouchHelper.Callback類
  • 19.自定義ItemDecoration分割線

    • 需要自定義類實現RecyclerView.ItemDecoration類,並選擇重寫合適方法
  • 21.RecyclerView優化處理

    • RecyclerView滑動卡頓原因有哪些?如何解決巢狀佈局滑動衝突?如何解決RecyclerView實現畫廊卡頓?
  • 22.RecyclerView問題彙總

    • getLayoutPosition()和getAdapterPosition()的區別
  • 23.RecyclerView滑動衝突

    • 01.如何判斷RecyclerView控制元件滑動到頂部和底部
    • 02.RecyclerView巢狀RecyclerView 條目自動上滾的Bug
    • 03.ScrollView巢狀RecyclerView滑動衝突
    • 04.ViewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager
    • 05.RecyclerView巢狀RecyclerView的滑動衝突問題
    • 06.RecyclerView使用Glide載入圖片導致圖片錯亂問題解決
  • 24.ScrollView巢狀RecyclerView問題

    • 要實現在NestedScrollView中嵌入一個或多個RecyclerView,會出現滑動衝突,焦點搶佔,顯示不全等。如何處理?
  • 25.RecyclerView封裝庫和綜合案例

    • 自定義支援上拉載入更多【載入中,載入失敗[比如沒有更多資料],載入異常[無網路],載入成功等多種狀態】,下拉重新整理,可以實現複雜的狀態頁面,支援自由切換狀態【載入中,載入成功,載入失敗,沒網路等狀態】的控制元件,擴充功能[支援長按拖拽,側滑刪除]可以選擇性新增。具體使用方法,可以直接參考demo案例。

25.0.0.0 請說一下RecyclerView?adapter的作用是什麼,幾個方法是做什麼用的?如何理解adapter訂閱者模式?

  • 關於RecyclerView,大家都已經很熟悉了,用途十分廣泛,大概結構如下所示
    • RecyclerView.Adapter - 處理資料集合並負責繫結檢視
    • ViewHolder - 持有所有的用於繫結資料或者需要操作的View
    • LayoutManager - 負責擺放檢視等相關操作
    • ItemDecoration - 負責繪製Item附近的分割線
    • ItemAnimator - 為Item的一般操作新增動畫效果,如,增刪條目等
  • 如圖所示,直觀展示結構
    • image
  • adapter的作用是什麼
    • RecyclerView.Adapter扮演的角色
    • 一是,根據不同ViewType建立與之相應的的Item-Layout
    • 二是,訪問資料集合並將資料繫結到正確的View上
  • 幾個方法是做什麼用的
    • 一般常用的重寫方法有以下這麼幾個:部落格
    public VH onCreateViewHolder(ViewGroup parent, int viewType)
    建立Item檢視,並返回相應的ViewHolder
    public void onBindViewHolder(VH holder, int position)
    繫結資料到正確的Item檢視上。
    public int getItemCount()
    返回該Adapter所持有的Itme數量
    public int getItemViewType(int position)
    用來獲取當前項Item(position引數)是哪種型別的佈局
    複製程式碼
  • 如何理解adapter訂閱者模式
    • 當時據集合發生改變時,我們通過呼叫.notifyDataSetChanged(),來重新整理列表,因為這樣做會觸發列表的重繪。
    • 注意這裡需要理解什麼是訂閱者模式……
    • a.首先看.notifyDataSetChanged()原始碼
      public final void notifyDataSetChanged() {
          mObservable.notifyChanged();
      }
      複製程式碼
    • b.接著檢視.notifyChanged();原始碼
      • 被觀察者AdapterDataObservable,內部持有觀察者AdapterDataObserver集合
      static class AdapterDataObservable extends Observable<AdapterDataObserver> {
          public boolean hasObservers() {
              return !mObservers.isEmpty();
          }
      
          public void notifyChanged() {
              for (int i = mObservers.size() - 1; i >= 0; i--) {
                  mObservers.get(i).onChanged();
              }
          }
      
          public void notifyItemRangeChanged(int positionStart, int itemCount) {
              notifyItemRangeChanged(positionStart, itemCount, null);
          }
      
          public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
              for (int i = mObservers.size() - 1; i >= 0; i--) {
                  mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
              }
          }
      
          public void notifyItemRangeInserted(int positionStart, int itemCount) {
              for (int i = mObservers.size() - 1; i >= 0; i--) {
                  mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
              }
          }
      }
      複製程式碼
      • 觀察者AdapterDataObserver,具體實現為RecyclerViewDataObserver,當資料來源發生變更時,及時響應介面變化
      public static abstract class AdapterDataObserver {
          public void onChanged() {
              // Do nothing
          }
      
          public void onItemRangeChanged(int positionStart, int itemCount) {
              // do nothing
          }
      
          public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
              onItemRangeChanged(positionStart, itemCount);
          }
      }
      複製程式碼
    • c.接著檢視setAdapter()原始碼中的setAdapterInternal(adapter, false, true)方法
      public void setAdapter(Adapter adapter) {
          // bail out if layout is frozen
          setLayoutFrozen(false);
          setAdapterInternal(adapter, false, true);
          requestLayout();
      }
      複製程式碼
      • setAdapterInternal(adapter, false, true)原始碼
      private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
              boolean removeAndRecycleViews) {
          if (mAdapter != null) {
              mAdapter.unregisterAdapterDataObserver(mObserver);
              mAdapter.onDetachedFromRecyclerView(this);
          }
          if (!compatibleWithPrevious || removeAndRecycleViews) {
              removeAndRecycleViews();
          }
          mAdapterHelper.reset();
          final Adapter oldAdapter = mAdapter;
          mAdapter = adapter;
          if (adapter != null) {
              //註冊一個觀察者RecyclerViewDataObserver
              adapter.registerAdapterDataObserver(mObserver);
              adapter.onAttachedToRecyclerView(this);
          }
          if (mLayout != null) {
              mLayout.onAdapterChanged(oldAdapter, mAdapter);
          }
          mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
          mState.mStructureChanged = true;
          markKnownViewsInvalid();
      }
      複製程式碼
    • d.notify……方法被呼叫,重新整理資料
      • 當資料變更時,呼叫notify**方法時,Adapter內部的被觀察者會遍歷通知已經註冊的觀察者的對應方法,這時介面就會響應變更。部落格

25.0.0.1 ViewHolder的作用是什麼?如何理解ViewHolder的複用?什麼時候停止呼叫onCreateViewHolder?

  • ViewHolder作用大概有這些:
    • adapter應當擁有ViewHolder的子類,並且ViewHolder內部應當儲存一些子view,避免時間代價很大的findViewById操作
    • 其RecyclerView內部定義的ViewHolder類包含很多複雜的屬性,內部使用場景也有很多,而我們經常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一個新型別。item的ViewHolder時呼叫來建立一個ViewHolder,而onBindViewHolder()方法則當RecyclerView需要在特定位置的item展示資料時呼叫。部落格
  • 如何理解ViewHolder的複用
    • 在複寫RecyclerView.Adapter的時候,需要我們複寫兩個方法:部落格
      • onCreateViewHolder
      • onBindViewHolder
      • 這兩個方法從字面上看就是建立ViewHolder和繫結ViewHolder的意思
    • 複用機制是怎樣的?
      • 模擬場景:只有一種ViewType,上下滑動的時候需要的ViewHolder種類是隻有一種,但是需要的ViewHolder物件數量並不止一個。所以在後面建立了9個ViewHolder之後,需要的數量夠了,無論怎麼滑動,都只需要複用以前建立的物件就行了。那麼逗比程式設計師們思考一下,為什麼會出現這種情況呢
      • 看到了下面log之後,第一反應是在這個ViewHolder物件的數量“夠用”之後就停止呼叫onCreateViewHolder方法,但是onBindViewHolder方法每次都會呼叫的
      • image
    • 檢視一下createViewHolder原始碼
      • 發現這裡並沒有限制
      public final VH createViewHolder(ViewGroup parent, int viewType) {
          TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
          final VH holder = onCreateViewHolder(parent, viewType);
          holder.mItemViewType = viewType;
          TraceCompat.endSection();
          return holder;
      }
      複製程式碼
  • 對於ViewHolder物件的數量“夠用”之後就停止呼叫onCreateViewHolder方法,可以檢視
    • 獲取為給定位置初始化的檢視。部落格
    • 此方法應由{@link LayoutManager}實現使用,以獲取檢視來表示來自{@LinkAdapter}的資料。
    • 如果共享池可用於正確的檢視型別,則回收程式可以重用共享池中的廢檢視或分離檢視。如果介面卡沒有指示給定位置上的資料已更改,則回收程式將嘗試發回一個以前為該資料初始化的報廢檢視,而不進行重新繫結。
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    
    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
        //程式碼省略了,有需要的小夥伴可以自己看看,這裡面邏輯實在太複雜呢
    }
    複製程式碼
25.0.0.2 ViewHolder封裝如何對findViewById優化?ViewHolder中為何使用SparseArray替代HashMap儲存viewId?
  • ViewHolder封裝如何對findViewById優化?
    class MyViewHolder extends RecyclerView.ViewHolder {
    
        private SparseArray<View> viewSparseArray;
        private TextView tvTitle;
    
        MyViewHolder(final View itemView) {
            super(itemView);
            if(viewSparseArray==null){
                viewSparseArray = new SparseArray<>();
            }
            tvTitle = (TextView) viewSparseArray.get(R.id.tv_title);
            if (tvTitle == null) {
                tvTitle = itemView.findViewById(R.id.tv_title);
                viewSparseArray.put(R.id.tv_title, tvTitle);
            }
        }
    }
    複製程式碼
  • 為何使用SparseArray替代HashMap儲存viewId
    • HashMap
      • 基本上就是一個 HashMap.Entry 的陣列(Entry 是 HashMap 的一個內部類)。更準確來說,Entry 類中包含以下欄位:
      • 一個非基本資料型別的 key
      • 一個非基本資料型別的 value
      • 儲存物件的雜湊值
      • 指向下一個 Entry 的指標
    • 當有鍵值對插入時,HashMap 會發生什麼 ?
      • 首先,鍵的雜湊值被計算出來,然後這個值會賦給 Entry 類中對應的 hashCode 變數。
      • 然後,使用這個雜湊值找到它將要被存入的陣列中“桶”的索引。
      • 如果該位置的“桶”中已經有一個元素,那麼新的元素會被插入到“桶”的頭部,next 指向上一個元素——本質上使“桶”形成連結串列。
    • 現在,當你用 key 去查詢值時,時間複雜度是 O(1)。雖然時間上 HashMap 更快,但同時它也花費了更多的記憶體空間。
    • 缺點:
      • 自動裝箱的存在意味著每一次插入都會有額外的物件建立。這跟垃圾回收機制一樣也會影響到記憶體的利用。
      • HashMap.Entry 物件本身是一層額外需要被建立以及被垃圾回收的物件。
      • “桶” 在 HashMap 每次被壓縮或擴容的時候都會被重新安排。這個操作會隨著物件數量的增長而變得開銷極大
      • 在Android中,當涉及到快速響應的應用時,記憶體至關重要,因為持續地分發和釋放記憶體會出發垃圾回收機制,這會拖慢應用執行。垃圾回收機制會影響應用效能表現,垃圾回收時間段內,應用程式是不會執行的,最終應用使用上就顯得卡頓。
    • SparseArray部落格
      • 它裡面也用了兩個陣列。一個int[] mKeys和Object[] mValues。從名字都可以看得出來一個用來儲存key一個用來儲存value的。
    • 當儲存一對鍵值對的時候:
      • key(不是它的hashcode)儲存在mKeys[]的下一個可用的位置上。所以不會再對key自動裝箱了。
      • value儲存在mValues[]的下一個位置上,value還是要自動裝箱的,如果它是基本型別。
    • 查詢的時候:
      • 查詢key還是用的二分法查詢。也就是說它的時間複雜度還是O(logN)
      • 知道了key的index,也就可以用key的index來從mValues中檢索出value。
    • 相較於HashMap,我們捨棄了Entry和Object型別的key,放棄了HashCode並依賴於二分法查詢。在新增和刪除操作的時候有更好的效能開銷。

25.0.0.3 LayoutManager作用是什麼?LayoutManager樣式有哪些?setLayoutManager原始碼裡做了什麼?

  • LayoutManager作用是什麼?
    • LayoutManager的職責是擺放Item的位置,並且負責決定何時回收和重用Item。部落格
    • RecyclerView 允許自定義規則去放置子 view,這個規則的控制者就是 LayoutManager。一個 RecyclerView 如果想展示內容,就必須設定一個 LayoutManager
  • LayoutManager樣式有哪些?
    • LinearLayoutManager 水平或者垂直的Item檢視。
    • GridLayoutManager 網格Item檢視。
    • StaggeredGridLayoutManager 交錯的網格Item檢視。
  • setLayoutManager(LayoutManager layout)原始碼
    • 分析:當之前設定過 LayoutManager 時,移除之前的檢視,並快取檢視在 Recycler 中,將新的 mLayout 物件與 RecyclerView 繫結,更新快取 View 的數量。最後去呼叫 requestLayout ,重新請求 measure、layout、draw。
    public void setLayoutManager(LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        // 停止滑動
        stopScroll();
        if (mLayout != null) {
            // 如果有動畫,則停止所有的動畫
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();
            }
            // 移除並回收檢視
            mLayout.removeAndRecycleAllViews(mRecycler);
            // 回收廢棄檢視
            mLayout.removeAndRecycleScrapInt(mRecycler);
            //清除mRecycler
            mRecycler.clear();
            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout +
                        " is already attached to a RecyclerView: " + layout.mRecyclerView);
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        //更新新的快取資料
        mRecycler.updateViewCacheSize();
        //重新請求 View 的測量、佈局、繪製
        requestLayout();
    }
    複製程式碼

25.0.0.4 SnapHelper主要是做什麼用的?SnapHelper是怎麼實現支援RecyclerView的對齊方式?

  • SnapHelper主要是做什麼用的
    • 在某些場景下,卡片列表滑動瀏覽[有的叫輪播圖],希望當滑動停止時可以將當前卡片停留在螢幕某個位置,比如停在左邊,以吸引使用者的焦點。那麼可以使用RecyclerView + Snaphelper來實現
  • SnapHelper是怎麼實現支援RecyclerView的對齊方式
    • SnapHelper旨在支援RecyclerView的對齊方式,也就是通過計算對齊RecyclerView中TargetView 的指定點或者容器中的任何畫素點。部落格
  • SnapHelper類重要的方法
    • attachToRecyclerView: 將SnapHelper attach 到指定的RecyclerView 上。
    • calculateDistanceToFinalSnap:複寫這個方法計算對齊到TargetView或容器指定點的距離,這是一個抽象方法,由子類自己實現,返回的是一個長度為2的int 陣列out,out[0]是x方向對齊要移動的距離,out[1]是y方向對齊要移動的距離。
    • calculateScrollDistance: 根據每個方向給定的速度估算滑動的距離,用於Fling 操作。
    • findSnapView:提供一個指定的目標View 來對齊,抽象方法,需要子類實現
    • findTargetSnapPosition:提供一個用於對齊的Adapter 目標position,抽象方法,需要子類自己實現。
    • onFling:根據給定的x和 y 軸上的速度處理Fling。
  • 什麼是Fling操作
    • 手指在螢幕上滑動 RecyclerView然後鬆手,RecyclerView中的內容會順著慣性繼續往手指滑動的方向繼續滾動直到停止,這個過程叫做 Fling 。 Fling 操作從手指離開螢幕瞬間被觸發,在滾動停止時結束。
  • LinearSnapHelper類分析
    • LinearSnapHelper 使當前Item居中顯示,常用場景是橫向的RecyclerView,類似ViewPager效果,但是又可以快速滑動(滑動多頁)。部落格
    • 最簡單的使用就是,如下程式碼
      • 幾行程式碼就可以用RecyclerView實現一個類似ViewPager的效果,並且效果還不錯。可以快速滑動多頁,當前頁劇中顯示,並且顯示前一頁和後一頁的部分。
      LinearSnapHelper snapHelper = new LinearSnapHelper();
      snapHelper.attachToRecyclerView(mRecyclerView);
      複製程式碼
  • PagerSnapHelper類分析
    • PagerSnapHelper看名字可能就能猜到,使RecyclerView像ViewPager一樣的效果,每次只能滑動一頁(LinearSnapHelper支援快速滑動), PagerSnapHelper也是Item居中對齊。
    • 最簡單的使用就是,如下程式碼
      PagerSnapHelper snapHelper = new PagerSnapHelper();
      snapHelper.attachToRecyclerView(mRecyclerView);
      複製程式碼

25.0.0.5 SpanSizeLookup的作用是幹什麼的?SpanSizeLookup如何使用?SpanSizeLookup實現原理如何理解?

  • SpanSizeLookup的作用是幹什麼的?
    • RecyclerView 可以通過 GridLayoutManager 實現網格佈局, 但是很少有人知道GridLayoutManager 還可以用來設定網格中指定Item的列數,類似於合併單元格的功能,而所有的這些我們僅僅只需通過定義一個RecycleView列表就可以完成,要實現指定某個item所佔列數的功能我們需要用到GridLayoutManager.SpanSizeLookup這個類,該類是一個抽象類,裡面包含了一個getSpanSize(int position)的抽象方法,該方法的返回值就是指定position所佔的列數
  • SpanSizeLookup如何使用?
    • 先是定義了一個6列的網格佈局,然後通過GridLayoutManager.SpanSizeLookup這個類來動態的指定某個item應該佔多少列。部落格
    • 比如getSpanSize返回6,就表示當前position索引處的item佔用6列,那麼顯示就只會展示一個ItemView【佔用6列】。
    • 比如getSpanSize返回3,就表示當前position索引處的item佔用3列
    GridLayoutManager manager = new GridLayoutManager(this, 6);
    manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            SpanModel model = mDataList.get(position);
            if (model.getType() == 1) {
                return 6;
            } else if(model.getType() == 2){
                return 3;
            }else if (model.getType() == 3){
                return 2;
            }else if (model.getType() == 4){
                return 2;
            } else {
                return 1;
            }
        }
    });
    複製程式碼

25.0.0.6 ItemDecoration的用途是什麼?自定義ItemDecoration有哪些重寫方法?分析一下addItemDecoration()原始碼?

  • ItemDecoration的用途是什麼?
    • 通過設定recyclerView.addItemDecoration(new DividerDecoration(this));來改變Item之間的偏移量或者對Item進行裝飾。
    • 當然,你也可以對RecyclerView設定多個ItemDecoration,列表展示的時候會遍歷所有的ItemDecoration並呼叫裡面的繪製方法,對Item進行裝飾。部落格
  • 自定義ItemDecoration有哪些重寫方法
    • 該抽象類常見的方法如下所示:部落格
    public void onDraw(Canvas c, RecyclerView parent)
    裝飾的繪製在Item條目繪製之前呼叫,所以這有可能被Item的內容所遮擋
    public void onDrawOver(Canvas c, RecyclerView parent)
    裝飾的繪製在Item條目繪製之後呼叫,因此裝飾將浮於Item之上
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
    與padding或margin類似,LayoutManager在測量階段會呼叫該方法,計算出每一個Item的正確尺寸並設定偏移量。
    複製程式碼
  • 分析一下addItemDecoration()原始碼?
    • a.通過下面程式碼可知,mItemDecorations是一個ArrayList,我們將ItemDecoration也就是分割線物件,新增到其中。
      • 可以看到,當通過這個方法新增分割線後,會指定新增分割線在集合中的索引,然後再重新請求 View 的測量、佈局、(繪製)。注意: requestLayout會呼叫onMeasure和onLayout,不一定呼叫onDraw!
      • 關於View自定義控制元件原始碼分析,可以參考我的其他部落格:github.com/yangchong21…
      public void addItemDecoration(ItemDecoration decor) {
          addItemDecoration(decor, -1);
      }
      
      //主要看這個方法,我的GitHub:https://github.com/yangchong211/YCBlogs
      public void addItemDecoration(ItemDecoration decor, int index) {
          if (mLayout != null) {
              mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                      + " layout");
          }
          if (mItemDecorations.isEmpty()) {
              setWillNotDraw(false);
          }
          if (index < 0) {
              mItemDecorations.add(decor);
          } else {
              // 指定新增分割線在集合中的索引
              mItemDecorations.add(index, decor);
          }
          markItemDecorInsetsDirty();
          // 重新請求 View 的測量、佈局、繪製
          requestLayout();
      }
      複製程式碼
      • 總結概括部落格
        • 可以看到在 View 的以上兩個方法中,分別呼叫了 ItemDecoration 物件的 onDraw onDrawOver 方法。
        • 這兩個抽象方法,由我們繼承 ItemDecoration 來自己實現,他們區別就是 onDraw 在 item view 繪製之前呼叫,onDrawOver 在 item view 繪製之後呼叫。
        • 所以繪製順序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。

25.0.0.7 上拉載入更多的功能是如何做的?新增滾動監聽事件需要注意什麼問題?網格佈局上拉載入如何優化?

  • 上拉載入更多的功能是如何做的?
    • 01.新增recyclerView的滑動事件
      • 首先給recyclerView新增滑動監聽事件。那麼我們知道,上拉載入時,需要具備兩個條件。第一個是監聽滑動到最後一個item,第二個是滑動到最後一個並且是向上滑動。
      • 設定滑動監聽器,RecyclerView自帶的ScrollListener,獲取最後一個完全顯示的itemPosition,然後判斷是否滑動到了最後一個item,
    • 02.上拉載入分頁資料
      • 然後開始呼叫更新上拉載入更多資料的方法。注意這裡的重新整理資料,可以直接用notifyItemRangeInserted方法,不要用notifyDataSetChanged方法。
    • 03.設定上拉載入的底部footer佈局
      • 在adapter中,可以上拉載入時處理footerView的邏輯
        • 在getItemViewType方法中設定最後一個Item為FooterView
        • 在onCreateViewHolder方法中根據viewType來載入不同的佈局
        • 最後在onBindViewHolder方法中設定一下載入的狀態顯示就可以
        • 由於多了一個FooterView,所以要記得在getItemCount方法的返回值中加上1。
    • 04.顯示和隱藏footer佈局
      • 一般情況下,滑動底部最後一個item,然後顯示footer上拉載入佈局,然後讓其載入500毫秒,最後載入出下一頁資料後再隱藏起來。部落格
  • 網格佈局上拉載入如何優化
    • 如果是網格佈局,那麼上拉重新整理的view則不是居中顯示,到載入更多的進度條顯示在了一個Item上,如果想要正常顯示的話,進度條需要橫跨兩個Item,這該怎麼辦呢?
    • 在adapter中的onAttachedToRecyclerView方法中處理網格佈局情況,程式碼如下所示,主要邏輯是如果當前是footer的位置,那麼該item佔據2個單元格,正常情況下佔據1個單元格。
    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) manager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    // 如果當前是footer的位置,那麼該item佔據2個單元格,正常情況下佔據1個單元格
                    return getItemViewType(position) == footType ? gridManager.getSpanCount() : 1;
                }
            });
        }
    }
    複製程式碼
  • 那麼如何實現自動進行上拉重新整理?
    • 設定滑動監聽,判斷是否滑動到底部,也就是最後一條資料,當滑動到最後時就開始載入下一頁資料,並且顯示載入下一頁loading。當載入資料成功後,則直接隱藏該佈局。
  • 那麼如何實現手動上拉重新整理呢?
    • 在上面步驟的基礎上進行修改,當滑動到最後一個資料時,展示上拉載入更多佈局。然後設定它的點選事件,點選之後開始載入下一頁資料,當載入完成後,則直接隱藏該佈局。

25.0.0.8 RecyclerView繪製原理如何理解?效能優化本質是什麼?RecyclerView繪製原理過程大概是怎樣的?

  • RecyclerView繪製原理如何理解?
    • image
  • 效能優化本質是什麼?
    • RecyclerView做效能優化要說複雜也複雜,比如說佈局優化,快取,預載入,複用池,重新整理資料等等。
      • 其優化的點很多,在這些看似獨立的點之間,其實存在一個樞紐:Adapter。因為所有的ViewHolder的建立和內容的繫結都需要經過Adapter的兩個函式onCreateViewHolder和onBindViewHolder。
    • 因此效能優化的本質就是要減少這兩個函式的呼叫時間和呼叫的次數。部落格
      • 如果我們想對RecyclerView做效能優化,必須清楚的瞭解到我們的每一步操作背後,onCreateViewHolder和onBindViewHolder呼叫了多少次。
  • RecyclerView繪製原理過程大概是怎樣的?
    • 簡化問題
      RecyclerView
          以LinearLayoutManager為例
          忽略ItemDecoration
          忽略ItemAnimator
          忽略Measure過程
          假設RecyclerView的width和height是確定的
      Recycler
          忽略mViewCacheExtension
      複製程式碼
    • 繪製過程
      • 類的職責介紹
        • LayoutManager:接管RecyclerView的Measure,Layout,Draw的過程
        • Recycler:快取池
        • Adapter:ViewHolder的生成器和內容繫結器。部落格
      • 繪製過程簡介
        • RecyclerView.requestLayout開始發生繪製,忽略Measure的過程
        • 在Layout的過程會通過LayoutManager.fill去將RecyclerView填滿
        • LayoutManager.fill會呼叫LayoutManager.layoutChunk去生成一個具體的ViewHolder
        • 然後LayoutManager就會呼叫Recycler.getViewForPosition向Recycler去要ViewHolder
        • Recycler首先去一級快取(Cache)裡面查詢是否命中,如果命中直接返回。如果一級快取沒有找到,則去三級快取查詢,如果三級快取找到了則呼叫Adapter.bindViewHolder來繫結內容,然後返回。如果三級快取沒有找到,那麼就通過Adapter.createViewHolder建立一個ViewHolder,然後呼叫Adapter.bindViewHolder繫結其內容,然後返回為Recycler。
        • 一直重複步驟3-5,知道建立的ViewHolder填滿了整個RecyclerView為止。

25.0.0.9 RecyclerView的Recyler是如何實現ViewHolder的快取?如何理解recyclerView三級快取是如何實現的?

  • RecyclerView的Recyler是如何實現ViewHolder的快取?
    • 首先看看程式碼
      public final class Recycler {
          final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
          ArrayList<ViewHolder> mChangedScrap = null;
          final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
          private final List<ViewHolder>
                  mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
          private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
          int mViewCacheMax = DEFAULT_CACHE_SIZE;
          RecycledViewPool mRecyclerPool;
          private ViewCacheExtension mViewCacheExtension;
          static final int DEFAULT_CACHE_SIZE = 2;
      }
      複製程式碼
    • RecyclerView在Recyler裡面實現ViewHolder的快取,Recycler裡面的實現快取的主要包含以下5個物件:
      • ArrayList mAttachedScrap:未與RecyclerView分離的ViewHolder列表,如果仍依賴於 RecyclerView (比如已經滑動出可視範圍,但還沒有被移除掉),但已經被標記移除的 ItemView 集合會被新增到 mAttachedScrap 中
        • 按照id和position來查詢ViewHolder
      • ArrayList mChangedScrap:表示資料已經改變的viewHolder列表,儲存 notifXXX 方法時需要改變的 ViewHolder,匹配機制按照position和id進行匹配
      • ArrayList mCachedViews:快取ViewHolder,主要用於解決RecyclerView滑動抖動時的情況,還有用於儲存Prefetch的ViewHoder
        • 最大的數量為:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache是由prefetch的時候計算出來的)
      • ViewCacheExtension mViewCacheExtension:開發者可自定義的一層快取,是虛擬類ViewCacheExtension的一個例項,開發者可實現方法getViewForPositionAndType(Recycler recycler, int position, int type)來實現自己的快取。
        • 位置固定
        • 內容不變
        • 數量有限
      • mRecyclerPool ViewHolder快取池,在有限的mCachedViews中如果存不下ViewHolder時,就會把ViewHolder存入RecyclerViewPool中。
        • 按照Type來查詢ViewHolder
        • 每個Type預設最多快取5個部落格
  • 如何理解recyclerView三級快取是如何實現的?
    • RecyclerView在設計的時候講上述5個快取物件分為了3級。每次建立ViewHolder的時候,會按照優先順序依次查詢快取建立ViewHolder。每次講ViewHolder快取到Recycler快取的時候,也會按照優先順序依次快取進去。三級快取分別是:
    • 一級快取:返回佈局和內容都都有效的ViewHolder
      • 按照position或者id進行匹配
      • 命中一級快取無需onCreateViewHolder和onBindViewHolder
      • mAttachScrap在adapter.notifyXxx的時候用到
      • mChanedScarp在每次View繪製的時候用到,因為getViewHolderForPosition非呼叫多次,後面將
      • mCachedView:用來解決滑動抖動的情況,預設值為2
    • 二級快取:返回View
      • 按照position和type進行匹配
      • 直接返回View
      • 需要自己繼承ViewCacheExtension實現
      • 位置固定,內容不發生改變的情況,比如說Header如果內容固定,就可以使用
    • 三級快取:返回佈局有效,內容無效的ViewHolder
      • 按照type進行匹配,每個type快取值預設=5
      • layout是有效的,但是內容是無效的
      • 多個RecycleView可共享,可用於多個RecyclerView的優化
  • 圖解部落格
    • image

25.0.1.0 螢幕滑動(狀態是item狀態可見,不可見,即將可見變化)時三級快取是如何理解的?adapter中的幾個方法是如何變化?

  • 螢幕滑動(狀態是item狀態可見,不可見,即將可見變化)時三級快取是如何理解的?
    • 如圖所示
      • image
    • 例項解釋:
      • 由於ViewCacheExtension在實際使用的時候較少用到,因此本例中忽略二級快取。mChangedScrap和mAttchScrap是RecyclerView內部控制的快取,本例暫時忽略。
      • 圖片解釋:
        • RecyclerView包含三部分:已經出螢幕,在螢幕裡面,即將進入螢幕,我們滑動的方向是向上
        • RecyclerView包含三種Type:1,2,3。螢幕裡面的都是Type=3
        • 紅色的線代表已經出螢幕的ViewHolder與Recycler的互動情況
        • 綠色的線代表,即將進入螢幕的ViewHolder進入螢幕時候,ViewHolder與Recycler的互動情況
      • 出螢幕時候的情況
        • 當ViewHolder(position=0,type=1)出螢幕的時候,由於mCacheViews是空的,那麼就直接放在mCacheViews裡面,ViewHolder在mCacheViews裡面佈局和內容都是有效的,因此可以直接複用。 ViewHolder(position=1,type=2)同步驟1
        • 當ViewHolder(position=2,type=1)出螢幕的時候由於一級快取mCacheViews已經滿了,因此將其放入RecyclerPool(type=1)的快取池裡面。此時ViewHolder的內容會被標記為無效,當其複用的時候需要再次通過Adapter.bindViewHolder來繫結內容。 ViewHolder(position=3,type=2)同步驟3
      • 進螢幕時候的情況部落格
        • 當ViewHolder(position=3-10,type=3)進入螢幕繪製的時候,由於Recycler的mCacheViews裡面找不到position匹配的View,同時RecyclerPool裡面找不到type匹配的View,因此,其只能通過adapter.createViewHolder來建立ViewHolder,然後通過adapter.bindViewHolder來繫結內容。
        • 當ViewHolder(position=11,type=1)進入螢幕的時候,發現ReccylerPool裡面能找到type=1的快取,因此直接從ReccylerPool裡面取來使用。由於內容是無效的,因此還需要呼叫bindViewHolder來繫結佈局。同時ViewHolder(position=4,type=3)需要出螢幕,其直接進入RecyclerPool(type=3)的快取池中
        • ViewHolder(position=12,type=2)同步驟6
      • 螢幕往下拉ViewHolder(position=1)進入螢幕的情況
        • 由於mCacheView裡面的有position=1的ViewHolder與之匹配,直接返回。由於內容是有效的,因此無需再次繫結內容
        • ViewHolder(position=0)同步驟8

25.0.1.1 SnapHelper有哪些重要的方法,其作用就是是什麼?LinearSnapHelper中是如何實現滾動停止的?

  • SnapHelper有哪些重要的方法,其作用就是是什麼?
    • calculateDistanceToFinalSnap抽象方法
      • 計算最終對齊要移動的距離
        • 計算二個引數對應的 ItemView 當前的座標與需要對齊的座標之間的距離。該方法返回一個大小為 2 的 int 陣列,分別對應out[0] 為 x 方向移動的距離,out[1] 為 y 方向移動的距離。
      @SuppressWarnings("WeakerAccess")
      @Nullable
      public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
              @NonNull View targetView);
      複製程式碼
    • findSnapView抽象方法
      • 找到要對齊的View
        • 該方法會找到當前 layoutManager 上最接近對齊位置的那個 view ,該 view 稱為 SanpView ,對應的 position 稱為 SnapPosition 。如果返回 null ,就表示沒有需要對齊的 View ,也就不會做滾動對齊調整。
      @SuppressWarnings("WeakerAccess")
      @Nullable
      public abstract View findSnapView(LayoutManager layoutManager);
      複製程式碼
    • findTargetSnapPosition抽象方法
      • 找到需要對齊的目標View的的Position。部落格
        • 更加詳細一點說就是該方法會根據觸發 Fling 操作的速率(引數 velocityX 和引數 velocityY )來找到 RecyclerView 需要滾動到哪個位置,該位置對應的 ItemView 就是那個需要進行對齊的列表項。我們把這個位置稱為 targetSnapPosition ,對應的 View 稱為 targetSnapView 。如果找不到 targetSnapPosition ,就返回RecyclerView.NO_POSITION 。
      public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
              int velocityY);
      複製程式碼
  • LinearSnapHelper中是如何實現滾動停止的?
    • SnapHelper繼承了 RecyclerView.OnFlingListener,實現了onFling方法。

      • 獲取RecyclerView要進行fling操作需要的最小速率,為啥呢?因為只有超過該速率,ItemView才會有足夠的動力在手指離開螢幕時繼續滾動下去。該方法返回的是一個布林值!
      @Override
      public boolean onFling(int velocityX, int velocityY) {
          LayoutManager layoutManager = mRecyclerView.getLayoutManager();
          if (layoutManager == null) {
              return false;
          }
          RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
          if (adapter == null) {
              return false;
          }
          int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
          return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                  && snapFromFling(layoutManager, velocityX, velocityY);
      }
      複製程式碼
    • 接著看看snapFromFling方法原始碼,就是通過該方法實現平滑滾動並使得在滾動停止時itemView對齊到目的座標位置

      • 首先layoutManager必須實現ScrollVectorProvider介面才能繼續往下操作
      • 然後通過createSnapScroller方法建立一個SmoothScroller,這個東西是一個平滑滾動器,用於對ItemView進行平滑滾動操作
      • 根據x和y方向的速度來獲取需要對齊的View的位置,需要子類實現
      • 最終通過 SmoothScroller 來滑動到指定位置部落格
      private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
              int velocityY) {
          if (!(layoutManager instanceof ScrollVectorProvider)) {
              return false;
          }
      
          RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
          if (smoothScroller == null) {
              return false;
          }
      
          int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
          if (targetPosition == RecyclerView.NO_POSITION) {
              return false;
          }
      
          smoothScroller.setTargetPosition(targetPosition);
          layoutManager.startSmoothScroll(smoothScroller);
          return true;
      }
      複製程式碼
      • 總結一下可知:snapFromFling()方法會先判斷layoutManager是否實現了ScrollVectorProvider介面,如果沒有實現該介面就不允許通過該方法做滾動操作。接下來就去建立平滑滾動器SmoothScroller的一個例項,layoutManager可以通過該平滑滾動器來進行滾動操作。SmoothScroller需要設定一個滾動的目標位置,將通過findTargetSnapPosition()方法來計算得到的targetSnapPosition給它,告訴滾動器要滾到這個位置,然後就啟動SmoothScroller進行滾動操作。
    • 接著看下createSnapScroller這個方法原始碼部落格

      • 先判斷layoutManager是否實現了ScrollVectorProvider這個介面,沒有實現該介面就不建立SmoothScroller
      • 這裡建立一個LinearSmoothScroller物件,然後返回給呼叫函式,也就是說,最終建立出來的平滑滾動器就是這個LinearSmoothScroller
      • 在建立該LinearSmoothScroller的時候主要考慮兩個方面:
        • 第一個是滾動速率,由calculateSpeedPerPixel()方法決定;
        • 第二個是在滾動過程中,targetView即將要進入到視野時,將勻速滾動變換為減速滾動,然後一直滾動目的座標位置,使滾動效果更真實,這是由onTargetFound()方法決定。
      @Nullable
      protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
          if (!(layoutManager instanceof ScrollVectorProvider)) {
              return null;
          }
          return new LinearSmoothScroller(mRecyclerView.getContext()) {
              @Override
              protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                  int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                          targetView);
                  final int dx = snapDistances[0];
                  final int dy = snapDistances[1];
                  final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                  if (time > 0) {
                      action.update(dx, dy, time, mDecelerateInterpolator);
                  }
              }
      
              @Override
              protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                  return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
              }
          };
      }
      複製程式碼

25.0.1.2 LinearSnapHelper程式碼中calculateDistanceToFinalSnap作用是什麼?那麼out[0]和out[1]分別指什麼?

  • calculateDistanceToFinalSnap的作用是什麼
    • 如果是水平方向滾動的,則計算水平方向需要移動的距離,否則水平方向的移動距離為0
    • 如果是豎直方向滾動的,則計算豎直方向需要移動的距離,否則豎直方向的移動距離為0
    • distanceToCenter方法主要作用是:計算水平或者豎直方向需要移動的距離
    @Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
    
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }
    複製程式碼
    • 接著看看distanceToCenter方法
      • 計算對應的view的中心座標到RecyclerView中心座標之間的距離
      • 首先是找到targetView的中心座標
      • 接著也就是找到容器【RecyclerView】的中心座標
      • 兩個中心座標的差值就是targetView需要滾動的距離
      private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
              @NonNull View targetView, OrientationHelper helper) {
          final int childCenter = helper.getDecoratedStart(targetView)
                  + (helper.getDecoratedMeasurement(targetView) / 2);
          final int containerCenter;
          if (layoutManager.getClipToPadding()) {
              containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
          } else {
              containerCenter = helper.getEnd() / 2;
          }
          return childCenter - containerCenter;
      }
      複製程式碼
  • 那麼out[0]和out[1]分別指什麼
    • 返回的是一個長度為2的int 陣列out,out[0]是x方向對齊要移動的距離,out[1]是y方向對齊要移動的距離。

25.0.1.3 如何實現可以設定分割線的顏色,寬度,以及到左右兩邊的寬度間距的自定義分割線,說一下思路?

  • 需要實現的分割線功能
    • 可以設定分割線的顏色,寬度,以及到左右兩邊的寬度間距。item預設分割線的顏色不可改變,那麼只有重寫onDraw方法,通過設定畫筆point顏色來繪製分割線顏色。而設定分割線左右的間隔是通過getItemOffsets方法實現的。
  • 幾個重要的方法說明
    • 需要自定義類實現RecyclerView.ItemDecoration類,並選擇重寫合適方法。注意下面這三個方法有著強烈的因果關係!
    //獲取當前view的位置資訊,該方法主要是設定條目周邊的偏移量
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
    //在item背後draw
    public void onDraw(Canvas c, RecyclerView parent, State state)
    //在item上邊draw
    public void onDrawOver(Canvas c, RecyclerView parent, State state)
    複製程式碼
  • 注意的是三個方法的呼叫順序
    • 首先呼叫的是getItemOffsets會被多次呼叫,在layoutManager每次測量可擺放的view的時候回撥用一次,在當前狀態下需要擺放多少個view這個方法就會回撥多少次。
    • 其次會呼叫onDraw方法,ItemDecoration的onDraw方法是在RecyclerView的onDraw方法中呼叫的,注意這時候傳入的canvas是RecyclerView的canvas,要時刻注意這點,它是和RecyclerView的邊界是一致的。這個時候繪製的內容相當於背景,會被item覆蓋。
    • 最後呼叫的是onDrawOver方法,ItemDecoration的onDrawOver方法是在RecyclerView的draw方法中呼叫的,同樣傳入的是RecyclerView的canvas,這時候onlayout已經呼叫,所以此時繪製的內容會覆蓋item。
  • 為每個item實現索引的思路
    • 要實現上面的可以設定分割線顏色和寬度,肯定是要繪製的,也就是需要使用到onDraw方法。那麼在getItemOffsets方法中需要讓view擺放位置距離bottom的距離是分割線的寬度。部落格
    • 然後通過parent.getChildCount()方法拿到當前顯示的view的數量[注意,該方法並不會獲取不顯示的view的數量],迴圈遍歷後,直接用paint畫筆進行繪製[注意至於分割線的顏色就是需要設定畫筆的顏色]。

25.0.1.4 如何實現複雜type首頁需求?如果不封裝會出現什麼問題和弊端?如何提高程式碼的簡便性和高效性?

  • 如何實現複雜type首頁需求
    • 通常寫一個多Item列表的方法
      • 根據不同的ViewType 處理不同的item,如果邏輯複雜,這個類的程式碼量是很龐大的。如果版本迭代新增新的需求,修改程式碼很麻煩,後期維護困難。
    • 主要操作步驟
      • 在onCreateViewHolder中根據viewType引數,也就是getItemViewType的返回值來判斷需要建立的ViewHolder型別
      • 在onBindViewHolder方法中對ViewHolder的具體型別進行判斷,分別為不同型別的ViewHolder進行繫結資料與邏輯處理
    • 程式碼如下所示
      public class HomeAdapter extends RecyclerView.Adapter {
          public static final int TYPE_BANNER = 0;
          public static final int TYPE_AD = 1;
          @Override
          public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
              switch (viewType){
                  case TYPE_BANNER:
                      return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_banner_layout,null));
                  case TYPE_AD:
                      return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_ad_item_layout,null));
              }
              return null;
          }
      
          @Override
          public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
              int type = getItemViewType(position);
              switch (type){
                  case TYPE_BANNER:
                      // banner 邏輯處理
                      break;
                  case TYPE_AD:
                      // 廣告邏輯處理
                      break;
                  // ... 此處省去N行程式碼
              }
          }
      
          @Override
          public int getItemViewType(int position) {
              if(position == 0){
                  return TYPE_BANNER;//banner在開頭
              }else {
                  return mData.get(position).type;//type 的值為TYPE_AD,TYPE_IMAGE,TYPE_AD,等其中一個
              }
          }
          public static class BannerViewHolder extends RecyclerView.ViewHolder{
              public BannerViewHolder(View itemView) {
                  super(itemView);
              }
          }
          public static class NewViewHolder extends RecyclerView.ViewHolder{
              public VideoViewHolder(View itemView) {
                  super(itemView);
              }
          }
      }
      複製程式碼
  • 如果不封裝會出現什麼問題和弊端
    • RecyclerView 可以用ViewType來區分不同的item,也可以滿足需求,但還是存在一些問題,比如:
      • 1,在item過多邏輯複雜列表介面,Adapter裡面的程式碼量龐大,邏輯複雜,後期難以維護。
      • 2,每次增加一個列表都需要增加一個Adapter,重複搬磚,效率低下。
      • 3,無法複用adapter,假如有多個頁面有多個type,那麼就要寫多個adapter。
      • 4,要是有區域性重新整理,那麼就比較麻煩了,比如廣告區也是一個九宮格的RecyclerView,點選區域性重新整理當前資料,比較麻煩。
    • 上面那樣寫的弊端
      • 型別檢查與型別轉型,由於在onCreateViewHolder根據不同型別建立了不同的ViewHolder,所以在onBindViewHolder需要針對不同型別的ViewHolder進行資料繫結與邏輯處理,這導致需要通過instanceof對ViewHolder進行型別檢查與型別轉型。
      • 不利於擴充套件,目前的需求是列表中存在5種佈局類型別,那麼如果需求變動,極端一點的情況就是資料來源是從伺服器獲取的,資料中的model決定列表中的佈局型別。這種情況下,每當model改變或model型別增加,我們都要去改變adapter中很多的程式碼,同時Adapter還必須知道特定的model在列表中的位置(position)除非跟服務端約定好,model(位置)不變,很顯然,這是不現實的。
      • 不利於維護,這點應該是上一點的延伸,隨著列表中佈局型別的增加與變更,getItemViewType、onCreateViewHolder、onBindViewHolder中的程式碼都需要變更或增加,Adapter 中的程式碼會變得臃腫與混亂,增加了程式碼的維護成本。
  • 如何提高程式碼的簡便性和高效性。具體封裝庫看:recyclerView複雜type封裝庫
    • 核心目的就是三個
      • 避免類的型別檢查與型別轉型
      • 增強Adapter的擴充套件性
      • 增強Adapter的可維護性
    • 當列表中型別增加或減少時Adapter中主要改動的就是getItemViewType、onCreateViewHolder、onBindViewHolder這三個方法,因此,我們就從這三個方法中開始著手。
    • 既然可能存在多個type型別的view,那麼能不能把這些比如banner,廣告,文字,視訊,新聞等當做一個HeaderView來操作。
    • 在getItemViewType方法中。
      • 減少if之類的邏輯判斷簡化程式碼,可以簡單粗暴的用hashCode作為增加type標識。
      • 通過建立列表的佈局型別,同時返回的不再是簡單的佈局型別標識,而是佈局的hashCode值
    • onCreateViewHolder
      • getItemViewType返回的是佈局hashCode值,也就是onCreateViewHolder(ViewGroup parent, int viewType)引數中的viewType
    • 在onBindViewHolder方法中。可以看到,在此方法中,新增一種header型別的view,則通過onBindView進行資料繫結。
    • 封裝後好處
      • 擴充性——Adapter並不關心不同的列表型別在列表中的位置,因此對於Adapter來說列表型別可以隨意增加或減少。十分方便,同時設定型別view的佈局和資料繫結都不需要在adapter中處理。充分解耦。
      • 可維護性——不同的列表型別由adapter新增headerView處理,哪怕新增多個headerView,相互之間互不干擾,程式碼簡潔,維護成本低。

25.0.1.5 關於item條目點選事件在onCreateViewHolder中寫和在onBindViewHolder中寫有何區別?如何優化?

  • 關於rv設定item條目點選事件有兩種方式:1.在onCreateViewHolder中寫;2.在onBindViewHolder中寫;3.在ViewHolder中寫。那麼究竟是哪一種好呢?
    • 1.在onCreateViewHolder中寫
      @NonNull
      @Override
      public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
          final View view = LayoutInflater.from(mContext).inflate(R.layout.item_me_gv_grid, parent, false);
          final MyViewHolder holder = new MyViewHolder(view);
          view.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  if (listener != null) {
                      listener.onItemClick(view, holder.getLayoutPosition());
                  }
              }
          });
          return holder;
      }
      複製程式碼
    • 2.在onBindViewHolder中寫
      @Override
      public void onBindViewHolder(@NonNull final MyViewHolder holder, int position) {
          holder.itemView.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  if (listener != null) {
                      listener.onItemClick(holder.itemView, holder.getAdapterPosition());
                  }
              }
          });
      }
      複製程式碼
  • onBindViewHolder() 中頻繁建立新的 onClickListener 例項沒有必要,建議實際開發中應該在 onCreateViewHolder() 中每次為新建的 View 設定一次就行。

25.0.1.6 RecyclerView滑動卡頓原因有哪些?如何解決巢狀佈局滑動衝突?如何解決RecyclerView實現畫廊卡頓?

  • RecyclerView滑動卡頓原因有哪些
    • 第一種:巢狀佈局滑動衝突
      • 導致巢狀滑動難處理的關鍵原因在於當子控制元件消費了事件, 那麼父控制元件就不會再有機會處理這個事件了, 所以一旦內部的滑動控制元件消費了滑動操作, 外部的滑動控制元件就再也沒機會響應這個滑動操作了
    • 第二種:巢狀佈局層次太深,比如六七層等
      • 測量,繪製佈局可能會導致滑動卡頓
    • 第三種:比如用RecyclerView實現畫廊,載入比較大的圖片,如果快速滑動,則可能會出現卡頓,主要是載入圖片需要時間
    • 第四種:在onCreateViewHolder或者在onBindViewHolder中做了耗時的操作導致卡頓。
  • 如何解決巢狀佈局滑動衝突
  • 如何解決RecyclerView實現畫廊卡頓?
    • RecyclerView 滑動時不讓 Glide 載入圖片。滾動停止後才開始恢復載入圖片。
    //RecyclerView.SCROLL_STATE_IDLE //空閒狀態
    //RecyclerView.SCROLL_STATE_FLING //滾動狀態
    //RecyclerView.SCROLL_STATE_TOUCH_SCROLL //觸控後狀態
    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                LoggerUtils.e("initRecyclerView"+ "恢復Glide載入圖片");
                Glide.with(ImageBrowseActivity.this).resumeRequests();
            }else {
                LoggerUtils.e("initRecyclerView"+"禁止Glide載入圖片");
                Glide.with(ImageBrowseActivity.this).pauseRequests();
            }
        }
    });
    複製程式碼
  • 在onCreateViewHolder或者在onBindViewHolder中做了耗時的操作導致卡頓
    • 按stackoverflow上面比較通俗的解釋:RecyclerView.Adapter裡面的onCreateViewHolder()方法和onBindViewHolder()方法對時間都非常敏感。類似I/O讀寫,Bitmap解碼一類的耗時操作,最好不要在它們裡面進行。

25.0.1.7 RecyclerView常見的優化有哪些?實際開發中都是怎麼做的,優化前後對比效能上有何提升?

  • RecyclerView常見的優化有哪些
    • DiffUtil重新整理優化
      • 分頁拉取遠端資料,對拉取下來的遠端資料進行快取,提升二次載入速度;對於新增或者刪除資料通過 DiffUtil 來進行區域性重新整理資料,而不是一味地全域性重新整理資料。
    • 佈局優化
      • 減少 xml 檔案 inflate 時間
        • 這裡的 xml 檔案不僅包括 layout 的 xml,還包括 drawable 的 xml,xml 檔案 inflate 出 ItemView 是通過耗時的 IO 操作,尤其當 Item 的複用機率很低的情況下,隨著 Type 的增多,這種 inflate 帶來的損耗是相當大的,此時我們可以用程式碼去生成佈局,即 new View() 的方式,只要搞清楚 xml 中每個節點的屬性對應的 API 即可。
      • 減少 View 物件的建立
        • 一個稍微複雜的 Item 會包含大量的 View,而大量的 View 的建立也會消耗大量時間,所以要儘可能簡化 ItemView;設計 ItemType 時,對多 ViewType 能夠共用的部分儘量設計成自定義 View,減少 View 的構造和巢狀。部落格
    • 對itemView中孩子View的點選事件優化
      • onBindViewHolder() 中頻繁建立新的 onClickListener 例項沒有必要,建議實際開發中應該在 onCreateViewHolder() 中每次為新建的 View 設定一次就行。
  • 其他的一些優化點
    • 如果 Item 高度是固定的話,可以使用 RecyclerView.setHasFixedSize(true); 來避免 requestLayout 浪費資源;
    • 設定 RecyclerView.addOnScrollListener(listener); 來對滑動過程中停止載入的操作。
    • 如果不要求動畫,可以通過 ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false); 把預設動畫關閉來提神效率。
    • 通過重寫 RecyclerView.onViewRecycled(holder) 來回收資源。
    • 通過 RecycleView.setItemViewCacheSize(size); 來加大 RecyclerView 的快取,用空間換時間來提高滾動的流暢性。
    • 如果多個 RecycledView 的 Adapter 是一樣的,比如巢狀的 RecyclerView 中存在一樣的 Adapter,可以通過設定 RecyclerView.setRecycledViewPool(pool); 來共用一個 RecycledViewPool。

25.0.1.8 如何解決RecyclerView巢狀RecyclerView條目自動上滾的Bug?如何解決ScrollView巢狀RecyclerView滑動衝突?

  • RecyclerView巢狀RecyclerView 條目自動上滾的Bug
    • RecyclerViewA巢狀RecyclerViewB 進入頁面自動跳轉到RecyclerViewB上面頁面會自動滾動。
    • 解決辦法如下所示
    • 一,recyclerview去除焦點
      • recyclerview.setFocusableInTouchMode(false);
      • recyclerview.requestFocus();
    • 二,在程式碼裡面 讓處於ScrollView或者RecyclerView1 頂端的某個控制元件獲得焦點即可
      • 比如頂部的一個textview
      • tv.setFocusableInTouchMode(true);
      • tv.requestFocus();
    • 三,可以直接在RecyclerView父佈局中新增上descendantFocusability屬性的值有三種:android:descendantFocusability="beforeDescendants"
      beforeDescendants:viewgroup會優先其子類控制元件而獲取到焦點
      afterDescendants:viewgroup只有當其子類控制元件不需要獲取焦點時才獲取焦點
      blocksDescendants:viewgroup會覆蓋子類控制元件而直接獲得焦點
      複製程式碼
  • 如何解決ScrollView巢狀RecyclerView滑動衝突?
    • 第一種方式:
      • 重寫父控制元件,讓父控制元件 ScrollView 直接攔截滑動事件,不向下分發給 RecyclerView,具體是定義一個ScrollView子類,重寫其 onInterceptTouchEvent()方法
      public class NoNestedScrollview extends NestedScrollView {
          @Override
          public boolean onInterceptTouchEvent(MotionEvent e) {
              int action = e.getAction();
              switch (action) {
                  case MotionEvent.ACTION_DOWN:
                      downX = (int) e.getRawX();
                      downY = (int) e.getRawY();
                      break;
                  case MotionEvent.ACTION_MOVE:
                      //判斷是否滑動,若滑動就攔截事件
                      int moveY = (int) e.getRawY();
                      if (Math.abs(moveY - downY) > mTouchSlop) {
                          return true;
                      }
                      break;
                  default:
                      break;
              }
              return super.onInterceptTouchEvent(e);
          }
      }
      複製程式碼
    • 第二種解決方式部落格
      • a.禁止RecyclerView滑動
      recyclerView.setLayoutManager(new GridLayoutManager(mContext,2){
          @Override
          public boolean canScrollVertically() {
              return false;
          }
          
          @Override
          public boolean canScrollHorizontally() {
              return super.canScrollHorizontally();
          }
      });
      
      recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayout.VERTICAL,false){
          @Override
          public boolean canScrollVertically() {
              return false;
          }
      });
      複製程式碼
    • 可能會出現的問題部落格
      • 雖然上面兩種方式解決了滑動衝突,但是有的手機上出現了RecyclerView會出現顯示不全的情況。
      • 針對這種情形,使用網上的方法一種是使用 RelativeLayout 包裹 RecyclerView 並設定屬性:android:descendantFocusability="blocksDescendants"
        • android:descendantFocusability="blocksDescendants",該屬>性是當一個view 獲取焦點時,定義 ViewGroup 和其子控制元件直接的關係,常用來>解決父控制元件的焦點或者點選事件被子空間獲取。
        • beforeDescendants: ViewGroup會優先其子控制元件獲取焦點
        • afterDescendants: ViewGroup只有當其子控制元件不需要獲取焦點時才獲取焦點
        • blocksDescendants: ViewGroup會覆蓋子類控制元件而直接獲得焦點
      • 相關程式碼案例:github.com/yangchong21…
      <RelativeLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:descendantFocusability="blocksDescendants">
          <android.support.v7.widget.RecyclerView
              android:id="@+id/rv_hot_review"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:foregroundGravity="center" />
      </RelativeLayout>
      複製程式碼

25.0.1.9 如何處理ViewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager?如何解決RecyclerView使用Glide載入圖片導致圖片錯亂問題?

  • ViewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager
    • 繼承RecyclerView,重寫dispatchTouchEvent,根據ACTION_MOVE的方向判斷是否呼叫getParent().requestDisallowInterceptTouchEvent去阻止父view攔截點選事件
    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
        /*---解決垂ViewPager巢狀直RecyclerView巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager start ---*/ 
        ViewParent parent = this; 
        while(!((parent = parent.getParent()) instanceof ViewPager));
        // 迴圈查詢viewPager 
        parent.requestDisallowInterceptTouchEvent(true); 
        return super.dispatchTouchEvent(ev); 
    }
    複製程式碼
  • 如何解決RecyclerView使用Glide載入圖片導致圖片錯亂問題
    • 為何會導致圖片載入後出現錯亂效果
      • 因為有ViewHolder的重用機制,每一個item在移除螢幕後都會被重新使用以節省資源,避免滑動卡頓。而在圖片的非同步載入過程中,從發出網路請求到完全下載並載入成Bitmap的圖片需要花費很長時間,而這時候很有可能原先需要載入圖片的item已經劃出介面並被重用了。而原先下載的圖片在被載入進ImageView的時候沒有判斷當前的ImageView是不是原先那個要求載入的,故可能圖片被載入到被重用的item上,就產生了圖片錯位的問題。解決思路也很簡單,就是在下載完圖片,準備給ImageView裝上的時候檢查一下這個ImageView。部落格
    • 第一種方法
      • 使用settag()方式,這種方式還是比較好的,但是,需要注意的是,Glide圖片載入也是使用將這個方法的,所以當你在Bindviewholder()使用時會直接拋異常,你需要使用settag(key,value)方式進行設定,這種方式是不錯的一種解決方式,注意取值的時候應該是gettag(key)這個方法哈,當非同步請求回來的時候對比下tag是否一樣在判斷是否顯示圖片。這邊直接複製博主的程式碼了。
      //給ImageView打上Tag作為特有標記
      imageView.setTag(tag);
       
      //下載圖片
      loadImage();
       
      //根據tag判斷是不是需要設定給ImageView
      if(tag == iamgeView.getTag()) {
          imageView.setBitmapImage(iamge);
      }
      複製程式碼

專案開源地址:github.com/yangchong21…

相關文章