你可能誤會(huì)了!原來(lái)自定義LayoutManager可以這么簡(jiǎn)單

參考資料

參考資料1;
參考資料2;
參考資料3
參考資料4;

背景介紹

RecyclerView由于其強(qiáng)大的擴(kuò)展性,現(xiàn)在已經(jīng)逐步的取代了ListViewGridView了。為了實(shí)現(xiàn)不同的布局效果,我們會(huì)用到官方提供的LinearLayoutManager、GridLayoutManagerStaggeredGridLayoutManager。但這些布局只能滿足日常需求,在一些比較復(fù)雜的布局中,它們就力不從心了,強(qiáng)行拼湊實(shí)現(xiàn),帶來(lái)的后果就是較差的體驗(yàn)和性能。所以能夠自定義LayoutManager還是十分必要的,它能夠解放創(chuàng)造力,構(gòu)造復(fù)雜的、流暢的滑動(dòng)列表。上面幾篇參考資料中就實(shí)現(xiàn)了一些不尋常的效果,我們可以看到,這些效果如果用常規(guī)的方案去實(shí)現(xiàn)將會(huì)十分蹩腳。

揭開(kāi)LayoutManager中不為人知的秘密

自定義LayoutManager主要要求我們完成三件事情:

  • 計(jì)算每個(gè)ItemView的位置;
  • 處理滑動(dòng)事件;
  • 緩存并重用ItemView;

而我們比較重要的工作是在onLayoutChildern() 這個(gè)回調(diào)方法中完成的。

下面我們就來(lái)一一解析。

預(yù)先準(zhǔn)備

當(dāng)我們extends RecyclerView.LayoutManager是,我們會(huì)被強(qiáng)制要求重寫(xiě)generateDefaultLayoutParams()方法,如方法名字一樣,我們需要提供一個(gè)默認(rèn)的LayoutParams,這里為我們的每個(gè)ItemView提供默認(rèn)的LayoutParams,所以它能夠直接影響到我們的布局效果,這里我們?cè)O(shè)置成WRAP_CONTENT,讓ItemView獲得決定權(quán)。

@Override
  public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
        RecyclerView.LayoutParams.WRAP_CONTENT);
  }

計(jì)算ItemView的位置

1.實(shí)現(xiàn)簡(jiǎn)單的LayoutManager

先看效果圖:

簡(jiǎn)單LayoutManager

再看代碼:

@Override
  public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    super.onLayoutChildren(recycler, state);
    // 先把所有的View先從RecyclerView中detach掉,然后標(biāo)記為"Scrap"狀態(tài),表示這些View處于可被重用狀態(tài)(非顯示中)。
    // 實(shí)際就是把View放到了Recycler中的一個(gè)集合中。
    detachAndScrapAttachedViews(recycler);
    calculateChildrenSite(recycler);
    // 回收和填充Item
    recycleAndFillView(recycler, state);
  }

private void calculateChildrenSite(RecyclerView.Recycler recycler) {
        totalHeight = 0;
        boolean needNew = true;
        int width = 0;
        int height = 0;
        for (int i = 0; i < getItemCount(); i++) {
            // 沒(méi)有會(huì)創(chuàng)建
            if (needNew) {
                View view = recycler.getViewForPosition(i);
                measureChildWithMargins(view, 0, 0);
                calculateItemDecorationsForChild(view, new Rect());
                width = getDecoratedMeasuredWidth(view);
                height = getDecoratedMeasuredHeight(view);
                addView(view);
            }
            if (totalHeight > getHeight() + height) {
                needNew = false;
            }
            Rect mTmpRect = allItemRects.get(i);
            if (mTmpRect == null) {
                mTmpRect = new Rect();
            }
            mTmpRect.set(0, totalHeight, DisplayUtils.getScreenWidth(), totalHeight + height);
            totalHeight = totalHeight + height;
            // 保存ItemView的位置信息
            allItemRects.put(i, mTmpRect);
            // 由于之前調(diào)用過(guò)detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見(jiàn)的
            itemStates.put(i, false);
        }
    }

這段代碼邏輯簡(jiǎn)單,它實(shí)現(xiàn)的其實(shí)就是一個(gè)簡(jiǎn)單的垂直線性布局,當(dāng)然現(xiàn)在還不能滑動(dòng),也沒(méi)有緩存機(jī)制。在這段代碼中,我們先調(diào)用detachAndScrapAttachedViews(recycler);將所有的ItemView標(biāo)記為Scrap狀態(tài),然后在挨個(gè)取出來(lái),計(jì)算他們應(yīng)該布局到什么位置,并用成員變量totalHeight記錄總高度,最后調(diào)用recycleAndFillView()將ItemView布局上去。

2.兩列式的LayoutManager

先看效果圖:

效果圖

有了上例的基礎(chǔ),我們只需要稍作調(diào)整,直接看下面代碼,注意注釋部分。

private void calculateChildrenSite(RecyclerView.Recycler recycler) {
        totalHeight = 0;
        boolean needNew = true;
        int width = 0;
        int height = 0;
        for (int i = 0; i < getItemCount(); i++) {
            // 沒(méi)有會(huì)創(chuàng)建
            if (needNew) {
                View view = recycler.getViewForPosition(i);
                measureChildWithMargins(view, DisplayUtils.getScreenWidth() / 2, 0);
                calculateItemDecorationsForChild(view, new Rect());
                width = getDecoratedMeasuredWidth(view);
                height = getDecoratedMeasuredHeight(view);
                addView(view);
            }
            if (totalHeight > getHeight() + height) {
                needNew = false;
            }
            Rect mTmpRect = allItemRects.get(i);
            if (mTmpRect == null) {
                mTmpRect = new Rect();
            }

            if (i % 2 == 0) { // 當(dāng)i能被2整除時(shí),是左,否則是右。
                // 左
                mTmpRect.set(0, totalHeight, DisplayUtils.getScreenWidth() / 2, totalHeight + height);
            } else {
                // 右,需要換行
                mTmpRect.set(DisplayUtils.getScreenWidth() / 2, totalHeight, DisplayUtils.getScreenWidth(),
                        totalHeight + height);
                totalHeight = totalHeight + height;
            }
            // 保存ItemView的位置信息
            allItemRects.put(i, mTmpRect);
            // 由于之前調(diào)用過(guò)detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見(jiàn)的
            itemStates.put(i, false);
        }

    }

處理滑動(dòng)

先來(lái)看一下效果:

效果圖

滑動(dòng)事件主要涉及到4個(gè)方法需要重寫(xiě),我們直接來(lái)看代碼:

@Override
  public boolean canScrollVertically() {
    //返回true表示可以縱向滑動(dòng)
    return true;
  }

  @Override
  public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    //列表向下滾動(dòng)dy為正,列表向上滾動(dòng)dy為負(fù),這點(diǎn)與Android坐標(biāo)系保持一致。
    //實(shí)際要滑動(dòng)的距離
    int travel = dy;

    LogUtils.e("dy = " + dy);
    //如果滑動(dòng)到最頂部
    if (verticalScrollOffset + dy < 0) {
      travel = -verticalScrollOffset;
    } else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {//如果滑動(dòng)到最底部
      travel = totalHeight - getVerticalSpace() - verticalScrollOffset;
    }

    //將豎直方向的偏移量+travel
    verticalScrollOffset += travel;

    // 調(diào)用該方法通知view在y方向上移動(dòng)指定距離
    offsetChildrenVertical(-travel);

    return travel;
  }

  private int getVerticalSpace() {
    //計(jì)算RecyclerView的可用高度,除去上下Padding值
    return getHeight() - getPaddingBottom() - getPaddingTop();
  }

  @Override
  public boolean canScrollHorizontally() {
    //返回true表示可以橫向滑動(dòng)
    return super.canScrollHorizontally();
  }

  @Override
  public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    //在這個(gè)方法中處理水平滑動(dòng)
    return super.scrollHorizontallyBy(dx, recycler, state);
  }

緩存并重用ItemView

在上面代碼的基礎(chǔ)上我們稍作改動(dòng),加入緩存,先看下面的log信息,它顯示雖然有100個(gè)Item,但childCount穩(wěn)定在26:

log

下面來(lái)看看代碼的變化,我展示了完整的代碼,留心注釋。

public class CustomLayoutManager extends RecyclerView.LayoutManager {
  /** 用于保存item的位置信息 */
  private SparseArray<Rect> allItemRects = new SparseArray<>();
  /** 用于保存item是否處于可見(jiàn)狀態(tài)的信息 */
  private SparseBooleanArray itemStates = new SparseBooleanArray();

  public int totalHeight = 0;
  private int verticalScrollOffset;

  @Override
  public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT);
  }

  @Override
  public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() <= 0 || state.isPreLayout()) {
      return;
    }
    super.onLayoutChildren(recycler, state);
    detachAndScrapAttachedViews(recycler);
    /* 這個(gè)方法主要用于計(jì)算并保存每個(gè)ItemView的位置 */
    calculateChildrenSite(recycler);
    recycleAndFillView(recycler, state);
  }

  private void calculateChildrenSite(RecyclerView.Recycler recycler) {
        totalHeight = 0;
        boolean needNew = true;
        int width = 0;
        int height = 0;
        for (int i = 0; i < getItemCount(); i++) {
            // 沒(méi)有會(huì)創(chuàng)建
            if (needNew) {
                View view = recycler.getViewForPosition(i);
                measureChildWithMargins(view, DisplayUtils.getScreenWidth() / 2, 0);
                calculateItemDecorationsForChild(view, new Rect());
                width = getDecoratedMeasuredWidth(view);
                height = getDecoratedMeasuredHeight(view);
                addView(view);
            }
            if (totalHeight > getHeight() + height) {
                needNew = false;
            }
            Rect mTmpRect = allItemRects.get(i);
            if (mTmpRect == null) {
                mTmpRect = new Rect();
            }

            if (i % 2 == 0) { // 當(dāng)i能被2整除時(shí),是左,否則是右。
                // 左
                mTmpRect.set(0, totalHeight, DisplayUtils.getScreenWidth() / 2, totalHeight + height);
            } else {
                // 右,需要換行
                mTmpRect.set(DisplayUtils.getScreenWidth() / 2, totalHeight, DisplayUtils.getScreenWidth(),
                        totalHeight + height);
                totalHeight = totalHeight + height;
            }
            // 保存ItemView的位置信息
            allItemRects.put(i, mTmpRect);
            // 由于之前調(diào)用過(guò)detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見(jiàn)的
            itemStates.put(i, false);
        }

    }


  private void recycleAndFillView(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() <= 0 || state.isPreLayout()) {
      return;
    }

    // 當(dāng)前scroll offset狀態(tài)下的顯示區(qū)域
    Rect displayRect= new Rect(0, verticalScrollOffset, getHorizontalSpace(),
        verticalScrollOffset + getVerticalSpace());

    /**
     * 將滑出屏幕的Items回收到Recycle緩存中
     */
    Rect childRect = new Rect();
    for (int i = 0; i < getItemCount(); i++) {
      //這個(gè)方法獲取的是RecyclerView中的View,注意區(qū)別Recycler中的View
      //這獲取的是實(shí)際的View
      View child = recycler.getViewForPosition(i);
      //下面幾個(gè)方法能夠獲取每個(gè)View占用的空間的位置信息,包括ItemDecorator
      childRect.left = getDecoratedLeft(child);
      childRect.top = getDecoratedTop(child);
      childRect.right = getDecoratedRight(child);
      childRect.bottom = getDecoratedBottom(child);
      //如果Item沒(méi)有在顯示區(qū)域,就說(shuō)明需要回收
      if (!Rect.intersects(displayRect, childRect)) {
        //移除并回收掉滑出屏幕的View
        removeAndRecycleView(child, recycler);
        itemStates.put(i, false); //更新該View的狀態(tài)為未依附
      }
    }

    //重新顯示需要出現(xiàn)在屏幕的子View
    for (int i = 0; i < getItemCount(); i++) {
      //判斷ItemView的位置和當(dāng)前顯示區(qū)域是否重合
      if (Rect.intersects(displayRect, allItemRects.get(i))) {
        //獲得Recycler中緩存的View
        View itemView = recycler.getViewForPosition(i);
        measureChildWithMargins(itemView, DisplayUtils.getScreenWidth() / 2, 0);
        //添加View到RecyclerView上
        addView(itemView);
        //取出先前存好的ItemView的位置矩形
        Rect rect = allItemRects.get(i);
        //將這個(gè)item布局出來(lái)
        layoutDecoratedWithMargins(itemView,
          rect.left,
          rect.top - verticalScrollOffset,  //因?yàn)楝F(xiàn)在是復(fù)用View,所以想要顯示在
          rect.right,
          rect.bottom - verticalScrollOffset);
        itemStates.put(i, true); //更新該View的狀態(tài)為依附
      }
    }
    LogUtils.e("itemCount = " + getChildCount());
  }


  @Override
  public boolean canScrollVertically() {
    // 返回true表示可以縱向滑動(dòng)
    return true;
  }

  @Override
  public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    //每次滑動(dòng)時(shí)先釋放掉所有的View,因?yàn)楹竺嬲{(diào)用recycleAndFillView()時(shí)會(huì)重新addView()。
    detachAndScrapAttachedViews(recycler);
    // 列表向下滾動(dòng)dy為正,列表向上滾動(dòng)dy為負(fù),這點(diǎn)與Android坐標(biāo)系保持一致。
    // 實(shí)際要滑動(dòng)的距離
    int travel = dy;

    LogUtils.e("dy = " + dy);
    // 如果滑動(dòng)到最頂部
    if (verticalScrollOffset + dy < 0) {
      travel = -verticalScrollOffset;
    } else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {// 如果滑動(dòng)到最底部
      travel = totalHeight - getVerticalSpace() - verticalScrollOffset;
    }
    // 調(diào)用該方法通知view在y方向上移動(dòng)指定距離
    offsetChildrenVertical(-travel);
    recycleAndFillView(recycler, state); //回收并顯示View
    // 將豎直方向的偏移量+travel
    verticalScrollOffset += travel;
    return travel;
  }

  private int getVerticalSpace() {
    // 計(jì)算RecyclerView的可用高度,除去上下Padding值
    return getHeight() - getPaddingBottom() - getPaddingTop();
  }

  @Override
  public boolean canScrollHorizontally() {
    // 返回true表示可以橫向滑動(dòng)
    return super.canScrollHorizontally();
  }

  @Override
  public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
      RecyclerView.State state) {
    // 在這個(gè)方法中處理水平滑動(dòng)
    return super.scrollHorizontallyBy(dx, recycler, state);
  }

  public int getHorizontalSpace() {
    return getWidth() - getPaddingLeft() - getPaddingRight();
  }
}

實(shí)現(xiàn)緩存最主要的就是先把每個(gè)ItemView的位置信息保存起來(lái),然后在滑動(dòng)過(guò)程中通過(guò)判斷每個(gè)ItemView的位置是否和當(dāng)前RecyclerView應(yīng)該顯示的區(qū)域有重合,若有就顯示它,若沒(méi)有就移除并回收。

總結(jié)

實(shí)現(xiàn)自己的自定義LayoutManager主要的三個(gè)步驟:

  • 計(jì)算每個(gè)ItemView的位置;
  • 添加滑動(dòng)事件;
  • 實(shí)現(xiàn)緩存。

我們需根據(jù)代碼多理解,多思考,然后動(dòng)手寫(xiě)屬于自己的LayoutManager

探討

最近路上留意到很多三輪摩托老司機(jī)開(kāi)車(chē)十分的奔放,和拉力賽有得一拼。之前坐過(guò)幾次,坐的時(shí)候因?yàn)橼s時(shí)間,所以當(dāng)時(shí)感覺(jué)老司機(jī)好負(fù)責(zé)。但最近作為路人看,老司機(jī)開(kāi)車(chē)開(kāi)的太危險(xiǎn),強(qiáng)行搶道,瘋狂按喇叭...整個(gè)是橫沖直撞的態(tài)勢(shì)??傊X(jué)得很危險(xiǎn)。
這件事,你怎么看?

如果你覺(jué)得這篇文章對(duì)你有幫助的話,點(diǎn)贊走一走,再加個(gè)關(guān)注,互相交流下。

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

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,552評(píng)論 25 708
  • 基本使用RecyclerView的基本使用并不復(fù)雜,只需要提供一個(gè)RecyclerView.Apdater的實(shí)現(xiàn)用...
    龐哈哈哈12138閱讀 6,074評(píng)論 2 46
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,255評(píng)論 4 61
  • 我是詩(shī)人 我不是精神病 我是這個(gè)世界的神 詩(shī)人是一個(gè)獨(dú)特的存在 什么都可以做不是無(wú)病呻吟 一滴露珠可以發(fā)現(xiàn)春天的秘...
    香自苦寒閱讀 289評(píng)論 2 3
  • 這是一部很感人 很感性的電影,里面描寫(xiě)的不僅僅地震所帶來(lái)的改變,而是在不同時(shí)候,不同事情, 不同地方時(shí)的人性,人心...
    blair_c閱讀 104評(píng)論 0 0