RecyclerView問題匯總

目錄介紹

  • 25.0.0.0 請說一下RecyclerView?adapter的作用是什么,幾個方法是做什么用的?如何理解adapter訂閱者模式?
  • 25.0.0.1 ViewHolder的作用是什么?如何理解ViewHolder的復(fù)用?什么時候停止調(diào)用onCreateViewHolder?
  • 25.0.0.2 ViewHolder封裝如何對findViewById優(yōu)化?ViewHolder中為何使用SparseArray替代HashMap存儲viewId?
  • 25.0.0.3 LayoutManager作用是什么?LayoutManager樣式有哪些?setLayoutManager源碼里做了什么?
  • 25.0.0.4 SnapHelper主要是做什么用的?SnapHelper是怎么實現(xiàn)支持RecyclerView的對齊方式?
  • 25.0.0.5 SpanSizeLookup的作用是干什么的?SpanSizeLookup如何使用?SpanSizeLookup實現(xiàn)原理如何理解?
  • 25.0.0.6 ItemDecoration的用途是什么?自定義ItemDecoration有哪些重寫方法?分析一下addItemDecoration()源碼?
  • 25.0.0.7 上拉加載更多的功能是如何做的?添加滾動監(jiān)聽事件需要注意什么問題?網(wǎng)格布局上拉加載如何優(yōu)化?
  • 25.0.0.8 RecyclerView繪制原理如何理解?性能優(yōu)化本質(zhì)是什么?RecyclerView繪制原理過程大概是怎樣的?
  • 25.0.0.9 RecyclerView的Recyler是如何實現(xiàn)ViewHolder的緩存?如何理解recyclerView三級緩存是如何實現(xiàn)的?
  • 25.0.1.0 屏幕滑動(狀態(tài)是item狀態(tài)可見,不可見,即將可見變化)時三級緩存是如何理解的?adapter中的幾個方法是如何變化?
  • 25.0.1.1 SnapHelper有哪些重要的方法,其作用就是是什么?LinearSnapHelper中是如何實現(xiàn)滾動停止的?
  • 25.0.1.2 LinearSnapHelper代碼中calculateDistanceToFinalSnap作用是什么?那么out[0]和out[1]分別指什么?
  • 25.0.1.3 如何實現(xiàn)可以設(shè)置分割線的顏色,寬度,以及到左右兩邊的寬度間距的自定義分割線,說一下思路?
  • 25.0.1.4 如何實現(xiàn)復(fù)雜type首頁需求?如果不封裝會出現(xiàn)什么問題和弊端?如何提高代碼的簡便性和高效性?
  • 25.0.1.5 關(guān)于item條目點擊事件在onCreateViewHolder中寫和在onBindViewHolder中寫有何區(qū)別?如何優(yōu)化?
  • 25.0.1.6 RecyclerView滑動卡頓原因有哪些?如何解決嵌套布局滑動沖突?如何解決RecyclerView實現(xiàn)畫廊卡頓?
  • 25.0.1.7 RecyclerView常見的優(yōu)化有哪些?實際開發(fā)中都是怎么做的,優(yōu)化前后對比性能上有何提升?
  • 25.0.1.8 如何解決RecyclerView嵌套RecyclerView條目自動上滾的Bug?如何解決ScrollView嵌套RecyclerView滑動沖突?
  • 25.0.1.9 如何處理ViewPager嵌套水平RecyclerView橫向滑動到底后不滑動ViewPager?如何解決RecyclerView使用Glide加載圖片導(dǎo)致圖片錯亂問題?

給自己相個親

  • 個人相親信息:https://juejin.im/post/5cff2c8b5188252354278c70

  • 00.RecyclerView復(fù)雜封裝庫

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

    • RecycleView的結(jié)構(gòu),RecyclerView簡單用法介紹
  • 02.Adapter

    • RecyclerView.Adapter扮演的角色,一般常用的重寫方法說明,數(shù)據(jù)變更通知之觀察者模式,查看.notifyChanged();源碼
  • 03.ViewHolder

    • ViewHolder的作用,如何理解對于ViewHolder對象的數(shù)量“夠用”之后就停止調(diào)用onCreateViewHolder方法,ViewHolder簡單封裝
  • 04.LayoutManager

    • LayoutManager作用是什么?setLayoutManager源碼分析
  • 05.SnapHelper

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

  • 07.SpanSizeLookup

    • SpanSizeLookup如何使用,同時包含列表,2列的網(wǎng)格,3列的網(wǎng)格如何優(yōu)雅實現(xiàn)?
  • 08.ItemDecoration

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

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

    • 官方有一個默認(rèn)Item動畫類DafaultItemAnimator,其中DefaultItemAnimator繼承了SimpleItemAnimator,在繼承了RecyclerView.ItemAnimator,它是如何實現(xiàn)動畫呢?
  • 11.RecyclerView上拉加載

    • 添加recyclerView的滑動事件,上拉加載分頁數(shù)據(jù),設(shè)置上拉加載的底部footer布局,顯示和隱藏footer布局
  • 12.RecyclerView緩存原理

    • RecyclerView做性能優(yōu)化要說復(fù)雜也復(fù)雜,比如說布局優(yōu)化,緩存,預(yù)加載,復(fù)用池,刷新數(shù)據(jù)等等
  • 13.SnapHelper源碼分析

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

    • 自定義SnapHelper
  • 18.ItemTouchHelper 實現(xiàn)交互動畫

    • 需要自定義類實現(xiàn)ItemTouchHelper.Callback類
  • 19.自定義ItemDecoration分割線

    • 需要自定義類實現(xiàn)RecyclerView.ItemDecoration類,并選擇重寫合適方法
  • 21.RecyclerView優(yōu)化處理

    • RecyclerView滑動卡頓原因有哪些?如何解決嵌套布局滑動沖突?如何解決RecyclerView實現(xiàn)畫廊卡頓?
  • 22.RecyclerView問題匯總

    • getLayoutPosition()和getAdapterPosition()的區(qū)別
  • 23.RecyclerView滑動沖突

    • 01.如何判斷RecyclerView控件滑動到頂部和底部
    • 02.RecyclerView嵌套RecyclerView 條目自動上滾的Bug
    • 03.ScrollView嵌套RecyclerView滑動沖突
    • 04.ViewPager嵌套水平RecyclerView橫向滑動到底后不滑動ViewPager
    • 05.RecyclerView嵌套RecyclerView的滑動沖突問題
    • 06.RecyclerView使用Glide加載圖片導(dǎo)致圖片錯亂問題解決
  • 24.ScrollView嵌套RecyclerView問題

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

    • 自定義支持上拉加載更多【加載中,加載失敗[比如沒有更多數(shù)據(jù)],加載異常[無網(wǎng)絡(luò)],加載成功等多種狀態(tài)】,下拉刷新,可以實現(xiàn)復(fù)雜的狀態(tài)頁面,支持自由切換狀態(tài)【加載中,加載成功,加載失敗,沒網(wǎng)絡(luò)等狀態(tài)】的控件,拓展功能[支持長按拖拽,側(cè)滑刪除]可以選擇性添加。具體使用方法,可以直接參考demo案例。

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

  • 關(guān)于RecyclerView,大家都已經(jīng)很熟悉了,用途十分廣泛,大概結(jié)構(gòu)如下所示
    • RecyclerView.Adapter - 處理數(shù)據(jù)集合并負(fù)責(zé)綁定視圖
    • ViewHolder - 持有所有的用于綁定數(shù)據(jù)或者需要操作的View
    • LayoutManager - 負(fù)責(zé)擺放視圖等相關(guān)操作
    • ItemDecoration - 負(fù)責(zé)繪制Item附近的分割線
    • ItemAnimator - 為Item的一般操作添加動畫效果,如,增刪條目等
  • 如圖所示,直觀展示結(jié)構(gòu)
    • image
  • adapter的作用是什么
    • RecyclerView.Adapter扮演的角色
    • 一是,根據(jù)不同ViewType創(chuàng)建與之相應(yīng)的的Item-Layout
    • 二是,訪問數(shù)據(jù)集合并將數(shù)據(jù)綁定到正確的View上
  • 幾個方法是做什么用的
    • 一般常用的重寫方法有以下這么幾個:博客
    public VH onCreateViewHolder(ViewGroup parent, int viewType)
    創(chuàng)建Item視圖,并返回相應(yīng)的ViewHolder
    public void onBindViewHolder(VH holder, int position)
    綁定數(shù)據(jù)到正確的Item視圖上。
    public int getItemCount()
    返回該Adapter所持有的Itme數(shù)量
    public int getItemViewType(int position)
    用來獲取當(dāng)前項Item(position參數(shù))是哪種類型的布局
    
  • 如何理解adapter訂閱者模式
    • 當(dāng)時據(jù)集合發(fā)生改變時,我們通過調(diào)用.notifyDataSetChanged(),來刷新列表,因為這樣做會觸發(fā)列表的重繪。
    • 注意這里需要理解什么是訂閱者模式……
    • a.首先看.notifyDataSetChanged()源碼
      public final void notifyDataSetChanged() {
          mObservable.notifyChanged();
      }
      
    • b.接著查看.notifyChanged();源碼
      • 被觀察者AdapterDataObservable,內(nèi)部持有觀察者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,具體實現(xiàn)為RecyclerViewDataObserver,當(dāng)數(shù)據(jù)源發(fā)生變更時,及時響應(yīng)界面變化
      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……方法被調(diào)用,刷新數(shù)據(jù)
      • 當(dāng)數(shù)據(jù)變更時,調(diào)用notify**方法時,Adapter內(nèi)部的被觀察者會遍歷通知已經(jīng)注冊的觀察者的對應(yīng)方法,這時界面就會響應(yīng)變更。博客

25.0.0.1 ViewHolder的作用是什么?如何理解ViewHolder的復(fù)用?什么時候停止調(diào)用onCreateViewHolder?

  • ViewHolder作用大概有這些:
    • adapter應(yīng)當(dāng)擁有ViewHolder的子類,并且ViewHolder內(nèi)部應(yīng)當(dāng)存儲一些子view,避免時間代價很大的findViewById操作
    • 其RecyclerView內(nèi)部定義的ViewHolder類包含很多復(fù)雜的屬性,內(nèi)部使用場景也有很多,而我們經(jīng)常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一個新類型。item的ViewHolder時調(diào)用來創(chuàng)建一個ViewHolder,而onBindViewHolder()方法則當(dāng)RecyclerView需要在特定位置的item展示數(shù)據(jù)時調(diào)用。博客
  • 如何理解ViewHolder的復(fù)用
    • 在復(fù)寫RecyclerView.Adapter的時候,需要我們復(fù)寫兩個方法:博客
      • onCreateViewHolder
      • onBindViewHolder
      • 這兩個方法從字面上看就是創(chuàng)建ViewHolder和綁定ViewHolder的意思
    • 復(fù)用機制是怎樣的?
      • 模擬場景:只有一種ViewType,上下滑動的時候需要的ViewHolder種類是只有一種,但是需要的ViewHolder對象數(shù)量并不止一個。所以在后面創(chuàng)建了9個ViewHolder之后,需要的數(shù)量夠了,無論怎么滑動,都只需要復(fù)用以前創(chuàng)建的對象就行了。那么逗比程序員們思考一下,為什么會出現(xiàn)這種情況呢
      • 看到了下面log之后,第一反應(yīng)是在這個ViewHolder對象的數(shù)量“夠用”之后就停止調(diào)用onCreateViewHolder方法,但是onBindViewHolder方法每次都會調(diào)用的
      • image
    • 查看一下createViewHolder源代碼
      • 發(fā)現(xiàn)這里并沒有限制
      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對象的數(shù)量“夠用”之后就停止調(diào)用onCreateViewHolder方法,可以查看
    • 獲取為給定位置初始化的視圖。博客
    • 此方法應(yīng)由{@link LayoutManager}實現(xiàn)使用,以獲取視圖來表示來自{@LinkAdapter}的數(shù)據(jù)。
    • 如果共享池可用于正確的視圖類型,則回收程序可以重用共享池中的廢視圖或分離視圖。如果適配器沒有指示給定位置上的數(shù)據(jù)已更改,則回收程序?qū)L試發(fā)回一個以前為該數(shù)據(jù)初始化的報廢視圖,而不進行重新綁定。
    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) {
        //代碼省略了,有需要的小伙伴可以自己看看,這里面邏輯實在太復(fù)雜呢
    }
    
25.0.0.2 ViewHolder封裝如何對findViewById優(yōu)化?ViewHolder中為何使用SparseArray替代HashMap存儲viewId?
  • ViewHolder封裝如何對findViewById優(yōu)化?
    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 的數(shù)組(Entry 是 HashMap 的一個內(nèi)部類)。更準(zhǔn)確來說,Entry 類中包含以下字段:
      • 一個非基本數(shù)據(jù)類型的 key
      • 一個非基本數(shù)據(jù)類型的 value
      • 保存對象的哈希值
      • 指向下一個 Entry 的指針
    • 當(dāng)有鍵值對插入時,HashMap 會發(fā)生什么 ?
      • 首先,鍵的哈希值被計算出來,然后這個值會賦給 Entry 類中對應(yīng)的 hashCode 變量。
      • 然后,使用這個哈希值找到它將要被存入的數(shù)組中“桶”的索引。
      • 如果該位置的“桶”中已經(jīng)有一個元素,那么新的元素會被插入到“桶”的頭部,next 指向上一個元素——本質(zhì)上使“桶”形成鏈表。
    • 現(xiàn)在,當(dāng)你用 key 去查詢值時,時間復(fù)雜度是 O(1)。雖然時間上 HashMap 更快,但同時它也花費了更多的內(nèi)存空間。
    • 缺點:
      • 自動裝箱的存在意味著每一次插入都會有額外的對象創(chuàng)建。這跟垃圾回收機制一樣也會影響到內(nèi)存的利用。
      • HashMap.Entry 對象本身是一層額外需要被創(chuàng)建以及被垃圾回收的對象。
      • “桶” 在 HashMap 每次被壓縮或擴容的時候都會被重新安排。這個操作會隨著對象數(shù)量的增長而變得開銷極大
      • 在Android中,當(dāng)涉及到快速響應(yīng)的應(yīng)用時,內(nèi)存至關(guān)重要,因為持續(xù)地分發(fā)和釋放內(nèi)存會出發(fā)垃圾回收機制,這會拖慢應(yīng)用運行。垃圾回收機制會影響應(yīng)用性能表現(xiàn),垃圾回收時間段內(nèi),應(yīng)用程序是不會運行的,最終應(yīng)用使用上就顯得卡頓。
    • SparseArray博客
      • 它里面也用了兩個數(shù)組。一個int[] mKeys和Object[] mValues。從名字都可以看得出來一個用來存儲key一個用來保存value的。
    • 當(dāng)保存一對鍵值對的時候:
      • key(不是它的hashcode)保存在mKeys[]的下一個可用的位置上。所以不會再對key自動裝箱了。
      • value保存在mValues[]的下一個位置上,value還是要自動裝箱的,如果它是基本類型。
    • 查找的時候:
      • 查找key還是用的二分法查找。也就是說它的時間復(fù)雜度還是O(logN)
      • 知道了key的index,也就可以用key的index來從mValues中檢索出value。
    • 相較于HashMap,我們舍棄了Entry和Object類型的key,放棄了HashCode并依賴于二分法查找。在添加和刪除操作的時候有更好的性能開銷。

25.0.0.3 LayoutManager作用是什么?LayoutManager樣式有哪些?setLayoutManager源碼里做了什么?

  • LayoutManager作用是什么?
    • LayoutManager的職責(zé)是擺放Item的位置,并且負(fù)責(zé)決定何時回收和重用Item。博客
    • RecyclerView 允許自定義規(guī)則去放置子 view,這個規(guī)則的控制者就是 LayoutManager。一個 RecyclerView 如果想展示內(nèi)容,就必須設(shè)置一個 LayoutManager
  • LayoutManager樣式有哪些?
    • LinearLayoutManager 水平或者垂直的Item視圖。
    • GridLayoutManager 網(wǎng)格Item視圖。
    • StaggeredGridLayoutManager 交錯的網(wǎng)格Item視圖。
  • setLayoutManager(LayoutManager layout)源碼
    • 分析:當(dāng)之前設(shè)置過 LayoutManager 時,移除之前的視圖,并緩存視圖在 Recycler 中,將新的 mLayout 對象與 RecyclerView 綁定,更新緩存 View 的數(shù)量。最后去調(diào)用 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);
            }
        }
        //更新新的緩存數(shù)據(jù)
        mRecycler.updateViewCacheSize();
        //重新請求 View 的測量、布局、繪制
        requestLayout();
    }
    

25.0.0.4 SnapHelper主要是做什么用的?SnapHelper是怎么實現(xiàn)支持RecyclerView的對齊方式?

  • SnapHelper主要是做什么用的
    • 在某些場景下,卡片列表滑動瀏覽[有的叫輪播圖],希望當(dāng)滑動停止時可以將當(dāng)前卡片停留在屏幕某個位置,比如停在左邊,以吸引用戶的焦點。那么可以使用RecyclerView + Snaphelper來實現(xiàn)
  • SnapHelper是怎么實現(xiàn)支持RecyclerView的對齊方式
    • SnapHelper旨在支持RecyclerView的對齊方式,也就是通過計算對齊RecyclerView中TargetView 的指定點或者容器中的任何像素點。博客
  • SnapHelper類重要的方法
    • attachToRecyclerView: 將SnapHelper attach 到指定的RecyclerView 上。
    • calculateDistanceToFinalSnap:復(fù)寫這個方法計算對齊到TargetView或容器指定點的距離,這是一個抽象方法,由子類自己實現(xiàn),返回的是一個長度為2的int 數(shù)組out,out[0]是x方向?qū)R要移動的距離,out[1]是y方向?qū)R要移動的距離。
    • calculateScrollDistance: 根據(jù)每個方向給定的速度估算滑動的距離,用于Fling 操作。
    • findSnapView:提供一個指定的目標(biāo)View 來對齊,抽象方法,需要子類實現(xiàn)
    • findTargetSnapPosition:提供一個用于對齊的Adapter 目標(biāo)position,抽象方法,需要子類自己實現(xiàn)。
    • onFling:根據(jù)給定的x和 y 軸上的速度處理Fling。
  • 什么是Fling操作
    • 手指在屏幕上滑動 RecyclerView然后松手,RecyclerView中的內(nèi)容會順著慣性繼續(xù)往手指滑動的方向繼續(xù)滾動直到停止,這個過程叫做 Fling 。 Fling 操作從手指離開屏幕瞬間被觸發(fā),在滾動停止時結(jié)束。
  • LinearSnapHelper類分析
    • LinearSnapHelper 使當(dāng)前Item居中顯示,常用場景是橫向的RecyclerView,類似ViewPager效果,但是又可以快速滑動(滑動多頁)。博客
    • 最簡單的使用就是,如下代碼
      • 幾行代碼就可以用RecyclerView實現(xiàn)一個類似ViewPager的效果,并且效果還不錯。可以快速滑動多頁,當(dāng)前頁劇中顯示,并且顯示前一頁和后一頁的部分。
      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實現(xiàn)原理如何理解?

  • SpanSizeLookup的作用是干什么的?
    • RecyclerView 可以通過 GridLayoutManager 實現(xiàn)網(wǎng)格布局, 但是很少有人知道GridLayoutManager 還可以用來設(shè)置網(wǎng)格中指定Item的列數(shù),類似于合并單元格的功能,而所有的這些我們僅僅只需通過定義一個RecycleView列表就可以完成,要實現(xiàn)指定某個item所占列數(shù)的功能我們需要用到GridLayoutManager.SpanSizeLookup這個類,該類是一個抽象類,里面包含了一個getSpanSize(int position)的抽象方法,該方法的返回值就是指定position所占的列數(shù)
  • SpanSizeLookup如何使用?
    • 先是定義了一個6列的網(wǎng)格布局,然后通過GridLayoutManager.SpanSizeLookup這個類來動態(tài)的指定某個item應(yīng)該占多少列。博客
    • 比如getSpanSize返回6,就表示當(dāng)前position索引處的item占用6列,那么顯示就只會展示一個ItemView【占用6列】。
    • 比如getSpanSize返回3,就表示當(dāng)前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的用途是什么?
    • 通過設(shè)置recyclerView.addItemDecoration(new DividerDecoration(this));來改變Item之間的偏移量或者對Item進行裝飾。
    • 當(dāng)然,你也可以對RecyclerView設(shè)置多個ItemDecoration,列表展示的時候會遍歷所有的ItemDecoration并調(diào)用里面的繪制方法,對Item進行裝飾。博客
  • 自定義ItemDecoration有哪些重寫方法
    • 該抽象類常見的方法如下所示:博客
    public void onDraw(Canvas c, RecyclerView parent)
    裝飾的繪制在Item條目繪制之前調(diào)用,所以這有可能被Item的內(nèi)容所遮擋
    public void onDrawOver(Canvas c, RecyclerView parent)
    裝飾的繪制在Item條目繪制之后調(diào)用,因此裝飾將浮于Item之上
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
    與padding或margin類似,LayoutManager在測量階段會調(diào)用該方法,計算出每一個Item的正確尺寸并設(shè)置偏移量。
    
  • 分析一下addItemDecoration()源碼?
    • a.通過下面代碼可知,mItemDecorations是一個ArrayList,我們將ItemDecoration也就是分割線對象,添加到其中。
      • 可以看到,當(dāng)通過這個方法添加分割線后,會指定添加分割線在集合中的索引,然后再重新請求 View 的測量、布局、(繪制)。注意: requestLayout會調(diào)用onMeasure和onLayout,不一定調(diào)用onDraw!
      • 關(guān)于View自定義控件源碼分析,可以參考我的其他博客:https://github.com/yangchong211/YCBlogs
      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();
      }
      
      • 總結(jié)概括博客
        • 可以看到在 View 的以上兩個方法中,分別調(diào)用了 ItemDecoration 對象的 onDraw onDrawOver 方法。
        • 這兩個抽象方法,由我們繼承 ItemDecoration 來自己實現(xiàn),他們區(qū)別就是 onDraw 在 item view 繪制之前調(diào)用,onDrawOver 在 item view 繪制之后調(diào)用。
        • 所以繪制順序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。

25.0.0.7 上拉加載更多的功能是如何做的?添加滾動監(jiān)聽事件需要注意什么問題?網(wǎng)格布局上拉加載如何優(yōu)化?

  • 上拉加載更多的功能是如何做的?
    • 01.添加recyclerView的滑動事件
      • 首先給recyclerView添加滑動監(jiān)聽事件。那么我們知道,上拉加載時,需要具備兩個條件。第一個是監(jiān)聽滑動到最后一個item,第二個是滑動到最后一個并且是向上滑動。
      • 設(shè)置滑動監(jiān)聽器,RecyclerView自帶的ScrollListener,獲取最后一個完全顯示的itemPosition,然后判斷是否滑動到了最后一個item,
    • 02.上拉加載分頁數(shù)據(jù)
      • 然后開始調(diào)用更新上拉加載更多數(shù)據(jù)的方法。注意這里的刷新數(shù)據(jù),可以直接用notifyItemRangeInserted方法,不要用notifyDataSetChanged方法。
    • 03.設(shè)置上拉加載的底部footer布局
      • 在adapter中,可以上拉加載時處理footerView的邏輯
        • 在getItemViewType方法中設(shè)置最后一個Item為FooterView
        • 在onCreateViewHolder方法中根據(jù)viewType來加載不同的布局
        • 最后在onBindViewHolder方法中設(shè)置一下加載的狀態(tài)顯示就可以
        • 由于多了一個FooterView,所以要記得在getItemCount方法的返回值中加上1。
    • 04.顯示和隱藏footer布局
      • 一般情況下,滑動底部最后一個item,然后顯示footer上拉加載布局,然后讓其加載500毫秒,最后加載出下一頁數(shù)據(jù)后再隱藏起來。博客
  • 網(wǎng)格布局上拉加載如何優(yōu)化
    • 如果是網(wǎng)格布局,那么上拉刷新的view則不是居中顯示,到加載更多的進度條顯示在了一個Item上,如果想要正常顯示的話,進度條需要橫跨兩個Item,這該怎么辦呢?
    • 在adapter中的onAttachedToRecyclerView方法中處理網(wǎng)格布局情況,代碼如下所示,主要邏輯是如果當(dāng)前是footer的位置,那么該item占據(jù)2個單元格,正常情況下占據(jù)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) {
                    // 如果當(dāng)前是footer的位置,那么該item占據(jù)2個單元格,正常情況下占據(jù)1個單元格
                    return getItemViewType(position) == footType ? gridManager.getSpanCount() : 1;
                }
            });
        }
    }
    
  • 那么如何實現(xiàn)自動進行上拉刷新?
    • 設(shè)置滑動監(jiān)聽,判斷是否滑動到底部,也就是最后一條數(shù)據(jù),當(dāng)滑動到最后時就開始加載下一頁數(shù)據(jù),并且顯示加載下一頁loading。當(dāng)加載數(shù)據(jù)成功后,則直接隱藏該布局。
  • 那么如何實現(xiàn)手動上拉刷新呢?
    • 在上面步驟的基礎(chǔ)上進行修改,當(dāng)滑動到最后一個數(shù)據(jù)時,展示上拉加載更多布局。然后設(shè)置它的點擊事件,點擊之后開始加載下一頁數(shù)據(jù),當(dāng)加載完成后,則直接隱藏該布局。

25.0.0.8 RecyclerView繪制原理如何理解?性能優(yōu)化本質(zhì)是什么?RecyclerView繪制原理過程大概是怎樣的?

  • RecyclerView繪制原理如何理解?
    • image
  • 性能優(yōu)化本質(zhì)是什么?
    • RecyclerView做性能優(yōu)化要說復(fù)雜也復(fù)雜,比如說布局優(yōu)化,緩存,預(yù)加載,復(fù)用池,刷新數(shù)據(jù)等等。
      • 其優(yōu)化的點很多,在這些看似獨立的點之間,其實存在一個樞紐:Adapter。因為所有的ViewHolder的創(chuàng)建和內(nèi)容的綁定都需要經(jīng)過Adapter的兩個函數(shù)onCreateViewHolder和onBindViewHolder。
    • 因此性能優(yōu)化的本質(zhì)就是要減少這兩個函數(shù)的調(diào)用時間和調(diào)用的次數(shù)。博客
      • 如果我們想對RecyclerView做性能優(yōu)化,必須清楚的了解到我們的每一步操作背后,onCreateViewHolder和onBindViewHolder調(diào)用了多少次。
  • RecyclerView繪制原理過程大概是怎樣的?
    • 簡化問題
      RecyclerView
          以LinearLayoutManager為例
          忽略ItemDecoration
          忽略ItemAnimator
          忽略Measure過程
          假設(shè)RecyclerView的width和height是確定的
      Recycler
          忽略mViewCacheExtension
      
    • 繪制過程
      • 類的職責(zé)介紹
        • LayoutManager:接管RecyclerView的Measure,Layout,Draw的過程
        • Recycler:緩存池
        • Adapter:ViewHolder的生成器和內(nèi)容綁定器。博客
      • 繪制過程簡介
        • RecyclerView.requestLayout開始發(fā)生繪制,忽略Measure的過程
        • 在Layout的過程會通過LayoutManager.fill去將RecyclerView填滿
        • LayoutManager.fill會調(diào)用LayoutManager.layoutChunk去生成一個具體的ViewHolder
        • 然后LayoutManager就會調(diào)用Recycler.getViewForPosition向Recycler去要ViewHolder
        • Recycler首先去一級緩存(Cache)里面查找是否命中,如果命中直接返回。如果一級緩存沒有找到,則去三級緩存查找,如果三級緩存找到了則調(diào)用Adapter.bindViewHolder來綁定內(nèi)容,然后返回。如果三級緩存沒有找到,那么就通過Adapter.createViewHolder創(chuàng)建一個ViewHolder,然后調(diào)用Adapter.bindViewHolder綁定其內(nèi)容,然后返回為Recycler。
        • 一直重復(fù)步驟3-5,知道創(chuàng)建的ViewHolder填滿了整個RecyclerView為止。

25.0.0.9 RecyclerView的Recyler是如何實現(xiàn)ViewHolder的緩存?如何理解recyclerView三級緩存是如何實現(xiàn)的?

  • RecyclerView的Recyler是如何實現(xiàn)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里面實現(xiàn)ViewHolder的緩存,Recycler里面的實現(xiàn)緩存的主要包含以下5個對象:
      • ArrayList mAttachedScrap:未與RecyclerView分離的ViewHolder列表,如果仍依賴于 RecyclerView (比如已經(jīng)滑動出可視范圍,但還沒有被移除掉),但已經(jīng)被標(biāo)記移除的 ItemView 集合會被添加到 mAttachedScrap 中
        • 按照id和position來查找ViewHolder
      • ArrayList mChangedScrap:表示數(shù)據(jù)已經(jīng)改變的viewHolder列表,存儲 notifXXX 方法時需要改變的 ViewHolder,匹配機制按照position和id進行匹配
      • ArrayList mCachedViews:緩存ViewHolder,主要用于解決RecyclerView滑動抖動時的情況,還有用于保存Prefetch的ViewHoder
        • 最大的數(shù)量為:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache是由prefetch的時候計算出來的)
      • ViewCacheExtension mViewCacheExtension:開發(fā)者可自定義的一層緩存,是虛擬類ViewCacheExtension的一個實例,開發(fā)者可實現(xiàn)方法getViewForPositionAndType(Recycler recycler, int position, int type)來實現(xiàn)自己的緩存。
        • 位置固定
        • 內(nèi)容不變
        • 數(shù)量有限
      • mRecyclerPool ViewHolder緩存池,在有限的mCachedViews中如果存不下ViewHolder時,就會把ViewHolder存入RecyclerViewPool中。
        • 按照Type來查找ViewHolder
        • 每個Type默認(rèn)最多緩存5個博客
  • 如何理解recyclerView三級緩存是如何實現(xiàn)的?
    • RecyclerView在設(shè)計的時候講上述5個緩存對象分為了3級。每次創(chuàng)建ViewHolder的時候,會按照優(yōu)先級依次查詢緩存創(chuàng)建ViewHolder。每次講ViewHolder緩存到Recycler緩存的時候,也會按照優(yōu)先級依次緩存進去。三級緩存分別是:
    • 一級緩存:返回布局和內(nèi)容都都有效的ViewHolder
      • 按照position或者id進行匹配
      • 命中一級緩存無需onCreateViewHolder和onBindViewHolder
      • mAttachScrap在adapter.notifyXxx的時候用到
      • mChanedScarp在每次View繪制的時候用到,因為getViewHolderForPosition非調(diào)用多次,后面將
      • mCachedView:用來解決滑動抖動的情況,默認(rèn)值為2
    • 二級緩存:返回View
      • 按照position和type進行匹配
      • 直接返回View
      • 需要自己繼承ViewCacheExtension實現(xiàn)
      • 位置固定,內(nèi)容不發(fā)生改變的情況,比如說Header如果內(nèi)容固定,就可以使用
    • 三級緩存:返回布局有效,內(nèi)容無效的ViewHolder
      • 按照type進行匹配,每個type緩存值默認(rèn)=5
      • layout是有效的,但是內(nèi)容是無效的
      • 多個RecycleView可共享,可用于多個RecyclerView的優(yōu)化
  • 圖解博客
    • image

25.0.1.0 屏幕滑動(狀態(tài)是item狀態(tài)可見,不可見,即將可見變化)時三級緩存是如何理解的?adapter中的幾個方法是如何變化?

  • 屏幕滑動(狀態(tài)是item狀態(tài)可見,不可見,即將可見變化)時三級緩存是如何理解的?
    • 如圖所示
      • image
    • 實例解釋:
      • 由于ViewCacheExtension在實際使用的時候較少用到,因此本例中忽略二級緩存。mChangedScrap和mAttchScrap是RecyclerView內(nèi)部控制的緩存,本例暫時忽略。
      • 圖片解釋:
        • RecyclerView包含三部分:已經(jīng)出屏幕,在屏幕里面,即將進入屏幕,我們滑動的方向是向上
        • RecyclerView包含三種Type:1,2,3。屏幕里面的都是Type=3
        • 紅色的線代表已經(jīng)出屏幕的ViewHolder與Recycler的交互情況
        • 綠色的線代表,即將進入屏幕的ViewHolder進入屏幕時候,ViewHolder與Recycler的交互情況
      • 出屏幕時候的情況
        • 當(dāng)ViewHolder(position=0,type=1)出屏幕的時候,由于mCacheViews是空的,那么就直接放在mCacheViews里面,ViewHolder在mCacheViews里面布局和內(nèi)容都是有效的,因此可以直接復(fù)用。
          ViewHolder(position=1,type=2)同步驟1
        • 當(dāng)ViewHolder(position=2,type=1)出屏幕的時候由于一級緩存mCacheViews已經(jīng)滿了,因此將其放入RecyclerPool(type=1)的緩存池里面。此時ViewHolder的內(nèi)容會被標(biāo)記為無效,當(dāng)其復(fù)用的時候需要再次通過Adapter.bindViewHolder來綁定內(nèi)容。
          ViewHolder(position=3,type=2)同步驟3
      • 進屏幕時候的情況博客
        • 當(dāng)ViewHolder(position=3-10,type=3)進入屏幕繪制的時候,由于Recycler的mCacheViews里面找不到position匹配的View,同時RecyclerPool里面找不到type匹配的View,因此,其只能通過adapter.createViewHolder來創(chuàng)建ViewHolder,然后通過adapter.bindViewHolder來綁定內(nèi)容。
        • 當(dāng)ViewHolder(position=11,type=1)進入屏幕的時候,發(fā)現(xiàn)ReccylerPool里面能找到type=1的緩存,因此直接從ReccylerPool里面取來使用。由于內(nèi)容是無效的,因此還需要調(diào)用bindViewHolder來綁定布局。同時ViewHolder(position=4,type=3)需要出屏幕,其直接進入RecyclerPool(type=3)的緩存池中
        • ViewHolder(position=12,type=2)同步驟6
      • 屏幕往下拉ViewHolder(position=1)進入屏幕的情況
        • 由于mCacheView里面的有position=1的ViewHolder與之匹配,直接返回。由于內(nèi)容是有效的,因此無需再次綁定內(nèi)容
        • ViewHolder(position=0)同步驟8

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

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

      • 獲取RecyclerView要進行fling操作需要的最小速率,為啥呢?因為只有超過該速率,ItemView才會有足夠的動力在手指離開屏幕時繼續(xù)滾動下去。該方法返回的是一個布爾值!
      @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方法源代碼,就是通過該方法實現(xiàn)平滑滾動并使得在滾動停止時itemView對齊到目的坐標(biāo)位置

      • 首先layoutManager必須實現(xiàn)ScrollVectorProvider接口才能繼續(xù)往下操作
      • 然后通過createSnapScroller方法創(chuàng)建一個SmoothScroller,這個東西是一個平滑滾動器,用于對ItemView進行平滑滾動操作
      • 根據(jù)x和y方向的速度來獲取需要對齊的View的位置,需要子類實現(xiàn)
      • 最終通過 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;
      }
      
      • 總結(jié)一下可知:snapFromFling()方法會先判斷l(xiāng)ayoutManager是否實現(xiàn)了ScrollVectorProvider接口,如果沒有實現(xiàn)該接口就不允許通過該方法做滾動操作。接下來就去創(chuàng)建平滑滾動器SmoothScroller的一個實例,layoutManager可以通過該平滑滾動器來進行滾動操作。SmoothScroller需要設(shè)置一個滾動的目標(biāo)位置,將通過findTargetSnapPosition()方法來計算得到的targetSnapPosition給它,告訴滾動器要滾到這個位置,然后就啟動SmoothScroller進行滾動操作。
    • 接著看下createSnapScroller這個方法源碼博客

      • 先判斷l(xiāng)ayoutManager是否實現(xiàn)了ScrollVectorProvider這個接口,沒有實現(xiàn)該接口就不創(chuàng)建SmoothScroller
      • 這里創(chuàng)建一個LinearSmoothScroller對象,然后返回給調(diào)用函數(shù),也就是說,最終創(chuàng)建出來的平滑滾動器就是這個LinearSmoothScroller
      • 在創(chuàng)建該LinearSmoothScroller的時候主要考慮兩個方面:
        • 第一個是滾動速率,由calculateSpeedPerPixel()方法決定;
        • 第二個是在滾動過程中,targetView即將要進入到視野時,將勻速滾動變換為減速滾動,然后一直滾動目的坐標(biāo)位置,使?jié)L動效果更真實,這是由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方法
      • 計算對應(yīng)的view的中心坐標(biāo)到RecyclerView中心坐標(biāo)之間的距離
      • 首先是找到targetView的中心坐標(biāo)
      • 接著也就是找到容器【RecyclerView】的中心坐標(biāo)
      • 兩個中心坐標(biāo)的差值就是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 數(shù)組out,out[0]是x方向?qū)R要移動的距離,out[1]是y方向?qū)R要移動的距離。

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

  • 需要實現(xiàn)的分割線功能
    • 可以設(shè)置分割線的顏色,寬度,以及到左右兩邊的寬度間距。item默認(rèn)分割線的顏色不可改變,那么只有重寫onDraw方法,通過設(shè)置畫筆point顏色來繪制分割線顏色。而設(shè)置分割線左右的間隔是通過getItemOffsets方法實現(xiàn)的。
  • 幾個重要的方法說明
    • 需要自定義類實現(xiàn)RecyclerView.ItemDecoration類,并選擇重寫合適方法。注意下面這三個方法有著強烈的因果關(guān)系!
    //獲取當(dāng)前view的位置信息,該方法主要是設(shè)置條目周邊的偏移量
    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)
    
  • 注意的是三個方法的調(diào)用順序
    • 首先調(diào)用的是getItemOffsets會被多次調(diào)用,在layoutManager每次測量可擺放的view的時候回調(diào)用一次,在當(dāng)前狀態(tài)下需要擺放多少個view這個方法就會回調(diào)多少次。
    • 其次會調(diào)用onDraw方法,ItemDecoration的onDraw方法是在RecyclerView的onDraw方法中調(diào)用的,注意這時候傳入的canvas是RecyclerView的canvas,要時刻注意這點,它是和RecyclerView的邊界是一致的。這個時候繪制的內(nèi)容相當(dāng)于背景,會被item覆蓋。
    • 最后調(diào)用的是onDrawOver方法,ItemDecoration的onDrawOver方法是在RecyclerView的draw方法中調(diào)用的,同樣傳入的是RecyclerView的canvas,這時候onlayout已經(jīng)調(diào)用,所以此時繪制的內(nèi)容會覆蓋item。
  • 為每個item實現(xiàn)索引的思路
    • 要實現(xiàn)上面的可以設(shè)置分割線顏色和寬度,肯定是要繪制的,也就是需要使用到onDraw方法。那么在getItemOffsets方法中需要讓view擺放位置距離bottom的距離是分割線的寬度。博客
    • 然后通過parent.getChildCount()方法拿到當(dāng)前顯示的view的數(shù)量[注意,該方法并不會獲取不顯示的view的數(shù)量],循環(huán)遍歷后,直接用paint畫筆進行繪制[注意至于分割線的顏色就是需要設(shè)置畫筆的顏色]。

25.0.1.4 如何實現(xiàn)復(fù)雜type首頁需求?如果不封裝會出現(xiàn)什么問題和弊端?如何提高代碼的簡便性和高效性?

  • 如何實現(xiàn)復(fù)雜type首頁需求
    • 通常寫一個多Item列表的方法
      • 根據(jù)不同的ViewType 處理不同的item,如果邏輯復(fù)雜,這個類的代碼量是很龐大的。如果版本迭代添加新的需求,修改代碼很麻煩,后期維護困難。
    • 主要操作步驟
      • 在onCreateViewHolder中根據(jù)viewType參數(shù),也就是getItemViewType的返回值來判斷需要創(chuàng)建的ViewHolder類型
      • 在onBindViewHolder方法中對ViewHolder的具體類型進行判斷,分別為不同類型的ViewHolder進行綁定數(shù)據(jù)與邏輯處理
    • 代碼如下所示
      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);
              }
          }
      }
      
  • 如果不封裝會出現(xiàn)什么問題和弊端
    • RecyclerView 可以用ViewType來區(qū)分不同的item,也可以滿足需求,但還是存在一些問題,比如:
      • 1,在item過多邏輯復(fù)雜列表界面,Adapter里面的代碼量龐大,邏輯復(fù)雜,后期難以維護。
      • 2,每次增加一個列表都需要增加一個Adapter,重復(fù)搬磚,效率低下。
      • 3,無法復(fù)用adapter,假如有多個頁面有多個type,那么就要寫多個adapter。
      • 4,要是有局部刷新,那么就比較麻煩了,比如廣告區(qū)也是一個九宮格的RecyclerView,點擊局部刷新當(dāng)前數(shù)據(jù),比較麻煩。
    • 上面那樣寫的弊端
      • 類型檢查與類型轉(zhuǎn)型,由于在onCreateViewHolder根據(jù)不同類型創(chuàng)建了不同的ViewHolder,所以在onBindViewHolder需要針對不同類型的ViewHolder進行數(shù)據(jù)綁定與邏輯處理,這導(dǎo)致需要通過instanceof對ViewHolder進行類型檢查與類型轉(zhuǎn)型。
      • 不利于擴展,目前的需求是列表中存在5種布局類類型,那么如果需求變動,極端一點的情況就是數(shù)據(jù)源是從服務(wù)器獲取的,數(shù)據(jù)中的model決定列表中的布局類型。這種情況下,每當(dāng)model改變或model類型增加,我們都要去改變adapter中很多的代碼,同時Adapter還必須知道特定的model在列表中的位置(position)除非跟服務(wù)端約定好,model(位置)不變,很顯然,這是不現(xiàn)實的。
      • 不利于維護,這點應(yīng)該是上一點的延伸,隨著列表中布局類型的增加與變更,getItemViewType、onCreateViewHolder、onBindViewHolder中的代碼都需要變更或增加,Adapter 中的代碼會變得臃腫與混亂,增加了代碼的維護成本。
  • 如何提高代碼的簡便性和高效性。具體封裝庫看:recyclerView復(fù)雜type封裝庫
    • 核心目的就是三個
      • 避免類的類型檢查與類型轉(zhuǎn)型
      • 增強Adapter的擴展性
      • 增強Adapter的可維護性
    • 當(dāng)列表中類型增加或減少時Adapter中主要改動的就是getItemViewType、onCreateViewHolder、onBindViewHolder這三個方法,因此,我們就從這三個方法中開始著手。
    • 既然可能存在多個type類型的view,那么能不能把這些比如banner,廣告,文本,視頻,新聞等當(dāng)做一個HeaderView來操作。
    • 在getItemViewType方法中。
      • 減少if之類的邏輯判斷簡化代碼,可以簡單粗暴的用hashCode作為增加type標(biāo)識。
      • 通過創(chuàng)建列表的布局類型,同時返回的不再是簡單的布局類型標(biāo)識,而是布局的hashCode值
    • onCreateViewHolder
      • getItemViewType返回的是布局hashCode值,也就是onCreateViewHolder(ViewGroup parent, int viewType)參數(shù)中的viewType
    • 在onBindViewHolder方法中。可以看到,在此方法中,添加一種header類型的view,則通過onBindView進行數(shù)據(jù)綁定。
    • 封裝后好處
      • 拓展性——Adapter并不關(guān)心不同的列表類型在列表中的位置,因此對于Adapter來說列表類型可以隨意增加或減少。十分方便,同時設(shè)置類型view的布局和數(shù)據(jù)綁定都不需要在adapter中處理。充分解耦。
      • 可維護性——不同的列表類型由adapter添加headerView處理,哪怕添加多個headerView,相互之間互不干擾,代碼簡潔,維護成本低。

項目開源地址:https://github.com/yangchong211/YCRefreshView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 主要是在使用 RecyclerView 過程中遇到的細(xì)碎問題和解決方案。 簡單使用 LinearLayoutMan...
    三流之路閱讀 3,979評論 0 5
  • 這篇文章分三個部分,簡單跟大家講一下 RecyclerView 的常用方法與奇葩用法;工作原理與ListView比...
    LucasAdam閱讀 4,414評論 0 27
  • 今天閑來無事點開了《雙世寵妃》,之前覺得這戲有點“腦殘”,但看了兩集我覺得我也腦殘了,哈哈哈哈。
    03f21483bb9a閱讀 135評論 3 0
  • TintySpring構(gòu)建過程 Step1-簡單容器 最基本的容器最基本的容器我們需要的是一個能夠容納我們創(chuàng)建的B...
    ArthurPapa閱讀 158評論 0 0