1. 程式人生 > >自定義LayoutManager的詳解及其使用

自定義LayoutManager的詳解及其使用

RecyclerView不斷的普及,越來越多的人使用來代替傳統的ListView,GridView等,為了跟進時代也要不斷的學習RecyclerView的相關知識,下面就來了解一下RecyclerView的LayoutManger。

Recycler

RecyclerView內部有一個Recycler,顧名思義它就是一個回收的工具,當定義LayoutManager時,它可以訪問到一個Recycler的例項,從而用於來回收或者獲取View。當需要一個新的view時,呼叫getViewForPosition()這個方法,它會返回一個View,這個View可能是之前Recycler回收的View再利用,也可能是一個新的View。

兩級快取機制

Scrap和Recycle

在RecyclerView中有兩級快取機制:Scrap和Recycle。

Scrap Heap(垃圾堆)是一個輕量的集合,View不會經過介面卡而是直接返回給LayoutManager,當需要一個View時首先回去Scrap快取裡面找有沒有所需要的View,而這裡面的View已經綁定了需要的資料所以無需適配直接使用。

Recycle Pool(回收池)這裡面回收的View如果再次使用需要重新經過介面卡繫結資料,即呼叫onBindViewHolder()進行繫結資料,當然如果Recycle Pool裡面也沒有View就只有重新建立View。

Detach和Remove

我們可以通過Detach和Remove決定把View快取在Recycle或者Scrap。

使用Detach是把View快取在Scrap,這種快取方式可以方便如果還需要把快取的View新增進來的場景,可以明顯提高效率,可以呼叫detachAndScrapView()方法來實現。

Remove就是把View移除掉,放到Recycle裡面,以備後面的再次利用,呼叫方法removeAndRecycleView()實現。

具體採取何種方法還是要根據你具體的需求來呼叫。


下面寫個具體例子來實現如下效果





當然做法就是寫一個類來繼承RecyclerView.LayoutManager
首先看看幾個重要的方法


generateDefaultLayoutParams()


這是一個必須重寫的方法,當然僅僅實現這個方法不行,雖然能編譯通過。這個方法是給RecyclerView的子View建立一個預設的LayoutParams,實現起來也十分簡單。

onLayoutChildren
這個方法顯然是用於放置子view的位置,十分重要的一個方法。


canScrollVertically()和canScrollHorizontally()
若想要RecyclerView能水平或者豎直滾動這兩個方法需要重寫返回true


scrollVerticallyBy()和scrollHorizontallyBy()
在水平或者豎直滾動時會分別呼叫這兩個方法,dx,dy代表每次的增長值,返回值是真實移動的距離


下面貼出程式碼

package com.lzy.lzy_layoutmanager;

import android.graphics.Rect;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by lzy on 2016/10/18.
 */
public class NyLayoutManager extends RecyclerView.LayoutManager {

    private static final String TAG = "lzy";
    //儲存所有item的偏移資訊
    private SparseArrayCompat<Rect> itemFrames = new SparseArrayCompat<>();
    //總的高度和寬度
    private int mTotalHeight;
    private int mTotalWidth;

    private int verticalOffset;//豎直方向的偏移
    private int horizontalOffset;//水平方向的偏移

    @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) {
            return;
        }
        detachAndScrapAttachedViews(recycler);

        int totalHeight = 0;
        int totalWidth = 0;
        int offsetX = 0;
        int offsetY = 0;
        //計算每個item的位置資訊,儲存在itemFrames裡面
        for (int i = 0; i < getItemCount(); i++) {
            //從快取中取出
            View view = recycler.getViewForPosition(i);
            //新增到RecyclerView中
            addView(view);
            //測量
            measureChildWithMargins(view, 0, 0);
            //獲取測量後的寬高
            int height = getDecoratedMeasuredHeight(view);
            int width = getDecoratedMeasuredWidth(view);
            //把每一個子View的寬高加起來獲得總的
            totalHeight += height;
            totalWidth += width;
            //邊界資訊儲存到Rect裡面
            Rect rect = itemFrames.get(i);
            if (rect == null) {
                rect = new Rect();
            }

            rect.set(offsetX, offsetY, offsetX + width, offsetY + height);
            itemFrames.put(i, rect);
            //橫豎方向的偏移
            offsetX += width;
            offsetY += height;
        }
        mTotalHeight = Math.max(totalHeight, getVerticalSpace());
        mTotalWidth = Math.max(totalWidth, getHorizontalSpace());

        fill(recycler, state);

    }

    //回收不必要的view(超出螢幕的),取出需要的顯示出來
    private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //獲得螢幕的邊界資訊
        Rect displayFrame = new Rect(horizontalOffset, verticalOffset, horizontalOffset + getHorizontalSpace(),
                verticalOffset + getVerticalSpace());

        //滑出螢幕回收到快取中
        Rect childFrame = new Rect();
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            childFrame.left = getDecoratedLeft(view);
            childFrame.top = getDecoratedTop(view);
            childFrame.right = getDecoratedRight(view);
            childFrame.bottom = getDecoratedBottom(view);
            //判斷是否在顯示區域裡面
            if (!Rect.intersects(displayFrame, childFrame)) {
                removeAndRecycleView(view, recycler);
            }
        }
        //在螢幕上顯示出
        for (int i = 0; i < getItemCount(); i++) {
            if (Rect.intersects(displayFrame, itemFrames.get(i))) {//判斷是否在螢幕中
                View view = recycler.getViewForPosition(i);
                measureChildWithMargins(view, 0, 0);
                addView(view);
                Rect rect = itemFrames.get(i);
                layoutDecorated(view, rect.left - horizontalOffset, rect.top - verticalOffset,
                        rect.right - horizontalOffset, rect.bottom - verticalOffset);
            }
        }


    }

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public boolean canScrollHorizontally() {
        return true;
    }


    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachAndScrapAttachedViews(recycler);
        if (verticalOffset + dy < 0) {//滑動到最頂部
            dy = -verticalOffset;
        } else if (verticalOffset + dy > mTotalHeight - getVerticalSpace()) {//滑動到底部
            dy = mTotalHeight - getVerticalSpace() - verticalOffset;
        }

        offsetChildrenVertical(-dy);
        fill(recycler, state);
        verticalOffset += dy;
        return dy;
    }

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachAndScrapAttachedViews(recycler);
        if (horizontalOffset + dx < 0) {//滑動到最左邊
            dx = -horizontalOffset;
        } else if (horizontalOffset + dx > mTotalWidth - getHorizontalSpace()) {//滑動到最右邊
            dx = mTotalWidth - getHorizontalSpace() - horizontalOffset;
        }

        offsetChildrenHorizontal(-dx);
        fill(recycler, state);
        horizontalOffset += dx;
        return dx;
    }

    //獲取控制元件的豎直高度
    private int getVerticalSpace() {
        return getHeight() - getPaddingBottom() - getPaddingTop();
    }

    //獲取控制元件的水平寬度
    private int getHorizontalSpace() {
        return getWidth() - getPaddingLeft() - getPaddingRight();
    }
}

程式碼中都有註釋就不多說了。所以我們通過自定義的LayoutManager就可以實現各種我們所想要的效果了!


要實現自定義LayoutManager,首先實現generateDefaultLayoutParams()方法給child新增預設的LayoutParams,在onLayoutChildren這個方法裡面首先detach掉介面上的view快取到scrap裡面,然後重新進行佈局,呼叫getViewForPosition取出快取的view,新增到RecyclerView裡面並測量,接著把它的邊界資訊設定為我們所想要的樣子,通過layoutDecorated()方法把需要顯示的子view佈局到介面上。

如果需要滑動,把canScrollVertically()和canScrollHorizontally()按需返回true,重寫scrollVerticallyBy()或者scrollHorizontallyBy()方法,這兩個方法需要返回真實的偏移距離,返回的dx或者dy可能並不是真實的移動距離,因為當滑動到邊緣的時候真實移動距離可能就不是dx或者dy,所以在這個時候需要判斷處理,移動就呼叫offsetChildrenHorizontal()或者offsetChildrenVertical()實現,然後重新佈局到介面上就可以實現。

Demo下載