Android 自定义LayoutManager

实现自定义LayoutManager主要的4个步骤: 

  1. 指定默认的LayoutParams
  2. 计算每个ItemView的位置 
  3. 添加滑动事件
  4. 实现缓存

其中,主要在onLayoutChildern() 这个回调方法中实现主要功能。

指定默认的 LayoutParams

        当你继承LayoutManager之后,必须要重写generateDefaultLayoutParams()方法

        这个方法指定了每一个子view默认的LayoutParams,并且这个LayoutParams会在你调用getViewForPosition()返回子view前应用到这个子view。

// 重写generateDefaultLayoutParams()方法
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new
    RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT);
}

 

2.1 实现简单的LayoutManager

       在这段代码中,我们先调用detachAndScrapAttachedViews(recycler);将所有的ItemView标记为Scrap状态,然后在挨个取出来,计算他们应该布局到什么位置,并用成员变量totalHeight记录总高度,最后依次调用layoutDecorated()将ItemView布局上去。

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() <= 0 || state.isPreLayout()) {
        return;
    }
// 先把所有的View先从RecyclerView中detach掉,然后标记为"Scrap"状态,表示    // 这些View处于可被重用状态(非显示中)。
// 实际就是把View放到了Recycler中的一个集合中。
    super.onLayoutChildren(recycler, state);
    detachAndScrapAttachedViews(recycler);
/* 这个方法主要用于计算并保存每个ItemView的位置 */
    calculateChildrenSite(recycler);
    recycleAndFillView(recycler, state);
}

 
private void calculateChildrenSite(RecyclerView.Recycler recycler) {
    totalHeight = 0;
    for (int i = 0; i < getItemCount(); i++) {
        // 遍历Recycler中保存的View取出来
        View view = recycler.getViewForPosition(i);
        addView(view); // 因为刚刚进行了detach操作,所以现在可以重新添加
        measureChildWithMargins(view, 0, 0); // 通知测量view的margin值
        int width = getDecoratedMeasuredWidth(view); 
        // 计算view实际大小,包括了ItemDecorator中设置的偏移量。
        int height = getDecoratedMeasuredHeight(view);
        Rect mTmpRect = new Rect();
        // 调用这个方法能够调整ItemView的大小,以除去ItemDecorator。
        calculateItemDecorationsForChild(view, mTmpRect);
        // 调用这句我们指定了该View的显示区域,并将View显示上去,此时所有区域都用于显示View,
        //包括ItemDecorator设置的距离。
        layoutDecorated(view, 0, totalHeight, width, totalHeight + height);
        totalHeight += height;
    }
}

2.2 两列式的LayoutManager

       有了上例的基础,我们只需要稍作调整,直接看下面代码,注意注释部分。

private void calculateChildrenSite(RecyclerView.Recycler recycler) {
    totalHeight = 0;
    for (int i = 0; i < getItemCount(); i++) {
        View view = recycler.getViewForPosition(i);
        addView(view);
        //我们自己指定ItemView的尺寸。
        measureChildWithMargins(view, DisplayUtils.getScreenWidth() / 2, 0);
        int width = getDecoratedMeasuredWidth(view);
        int height = getDecoratedMeasuredHeight(view);
        Rect mTmpRect = new Rect();
        calculateItemDecorationsForChild(view, mTmpRect);
        if (i % 2 == 0) { //当i能被2整除时,是左,否则是右。
            //左
            layoutDecoratedWithMargins(view, 0, totalHeight, DisplayUtils.getScreenWidth() / 2,
                    totalHeight + height);
        } else {
            //右,需要换行
            layoutDecoratedWithMargins(view, DisplayUtils.getScreenWidth() / 2, totalHeight,
                    DisplayUtils.getScreenWidth(), totalHeight + height);
            totalHeight = totalHeight + height;
            LogUtils.e(i + "->" + totalHeight);
        }
    }
}

处理滑动

        滑动事件主要涉及到4个方法需要重写,我们直接来看代码:

@Override
public boolean canScrollVertically() {
    // 返回true表示可以纵向滑动
    return true;
}

 
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    //每次滑动时先释放掉所有的View,因为后面调用recycleAndFillView()时会重新addView()。
    detachAndScrapAttachedViews(recycler);
    // 列表向下滚动dy为正,列表向上滚动dy为负,这点与Android坐标系保持一致。
    // 实际要滑动的距离
    int travel = dy;

 
    LogUtils.e("dy = " + dy);
    // 如果滑动到最顶部
    if (verticalScrollOffset + dy < 0) {
        travel = -verticalScrollOffset;
    } else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {// 如果滑动到最底部
        travel = totalHeight - getVerticalSpace() - verticalScrollOffset;
    }
    // 调用该方法通知view在y方向上移动指定距离
    offsetChildrenVertical(-travel);
    recycleAndFillView(recycler, state); //回收并显示View
    // 将竖直方向的偏移量+travel
    verticalScrollOffset += travel;
    return travel;
}

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

 
@Override
public boolean canScrollHorizontally() {
    // 返回true表示可以横向滑动
    return super.canScrollHorizontally();
}

 
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 在这个方法中处理水平滑动
    return super.scrollHorizontallyBy(dx, recycler, state);
}

实现缓存

      缓存最主要的就是先把每个ItemView的位置信息保存起来,然后在滑动过程中通过判断每个ItemView的位置是否和当前RecyclerView应该显示的区域有重合,若有就显示它,若没有就移除并回收。

/**
 * 将滑出屏幕的Items回收到Recycle缓存中
 */
Rect childRect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
    //这个方法获取的是RecyclerView中的View,注意区别Recycler中的View
    //这获取的是实际的View
    View child = getChildAt(i);
    //下面几个方法能够获取每个View占用的空间的位置信息,包括ItemDecorator
    childRect.left = getDecoratedLeft(child);
    childRect.top = getDecoratedTop(child);
    childRect.right = getDecoratedRight(child);
    childRect.bottom = getDecoratedBottom(child);
    //如果Item没有在显示区域,就说明需要回收
    if (!Rect.intersects(displayRect, childRect)) {
    //移除并回收掉滑出屏幕的View
    removeAndRecycleView(child, recycler);
    itemStates.put(i, false); //更新该View的状态为未依附
    }
    }

 
    //重新显示需要出现在屏幕的子View
    for (int i = 0; i < getItemCount(); i++) {
    //判断ItemView的位置和当前显示区域是否重合
    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);
    //将这个item布局出来
    layoutDecoratedWithMargins(itemView,
    rect.left,
    rect.top - verticalScrollOffset,  //因为现在是复用View,所以想要显示在
    rect.right,
    rect.bottom - verticalScrollOffset);
    itemStates.put(i, true); //更新该View的状态为依附
    }
    }
    LogUtils.e("itemCount = " + getChildCount());
    }