自定义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()实现。

具体采取何种方法还是要根据你具体的需求来调用。


下面写个具体例子来实现如下效果

自定义LayoutManager的详解及其使用_第1张图片



当然做法就是写一个类来继承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 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下载


你可能感兴趣的:(Android,进阶)