创建 RecyclerView LayoutManager – Part 2

2357次阅读  |  发布于5年以前

创建 RecyclerView LayoutManager – Part 2

创建 a RecyclerView LayoutManager – Part 2

本文是这个系列中的 Part 2,这里是 Part 1Part 3 的链接。

上次我们讲了创建一个 RecyclerView LayoutManager 的核心步骤。接下来, 我们会介绍如何给普通基于适配器的 View 加入一些附加特性。

友情提醒:示例中的代码在这里 Github


Item Decorations 支持

RecyclerView 有一个很好的特性 RecyclerView.ItemDecoration,它可以给 子视图添加自定义样式,还可以在不修改子视图布局参数的情况下插入 布局属性(margins)。后者就是 LayoutManager 必须提供的约束子视图布局方式。


RecyclerPlayground 里有几个 decorator 用来介绍它们的实现方式。

LayoutManager 中提供了一些辅助方法操作 decorations ,不需要我们自己实现:

只要你使用了正确的方法去获取视图的属性和测量数据,RecyclerView 会自己搞定细节部分的处理。


数据集改变

当使用 notifyDataSetChanged()触发 RecyclerView.Adapter 的更新操作时, LayoutManager 负责更新布局中的视图。这时,onLayoutChildren()会被再次调用。 实现这个功能需要我们调整代码,判断出当前状态是生成一个新的视图 还是 adapter 更新期间的视图改变。下面是FixedGridLayoutManager中的填充方法的完整实现:

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //We have nothing to show for an empty data set but clear any existing views
    if (getItemCount() == 0) {
        detachAndScrapAttachedViews(recycler);
        return;
    }

    //...on empty layout, update child size measurements
    if (getChildCount() == 0) {
        //Scrap measure one child
        View scrap = recycler.getViewForPosition(0);
        addView(scrap);
        measureChildWithMargins(scrap, 0, 0);

        /*
         * We make some assumptions in this code based on every child
         * view being the same size (i.e. a uniform grid). This allows
         * us to compute the following values up front because they
         * won't change.
         */
        mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
        mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);

        detachAndScrapView(scrap, recycler);
    }

    updateWindowSizing();

    int childLeft;
    int childTop;
    if (getChildCount() == 0) { //First or empty layout
        /*
         * Reset the visible and scroll positions
         */
        mFirstVisiblePosition = 0;
        childLeft = childTop = 0;
    } else if (getVisibleChildCount() > getItemCount()) {
        //Data set is too small to scroll fully, just reset position
        mFirstVisiblePosition = 0;
        childLeft = childTop = 0;
    } else { //Adapter data set changes
        /*
         * Keep the existing initial position, and save off
         * the current scrolled offset.
         */
        final View topChild = getChildAt(0);
        if (mForceClearOffsets) {
            childLeft = childTop = 0;
            mForceClearOffsets = false;
        } else {
            childLeft = getDecoratedLeft(topChild);
            childTop = getDecoratedTop(topChild);
        }

        /*
         * Adjust the visible position if out of bounds in the
         * new layout. This occurs when the new item count in an adapter
         * is much smaller than it was before, and you are scrolled to
         * a location where no items would exist.
         */
        int lastVisiblePosition = positionOfIndex(getVisibleChildCount() - 1);
        if (lastVisiblePosition >= getItemCount()) {
            lastVisiblePosition = (getItemCount() - 1);
            int lastColumn = mVisibleColumnCount - 1;
            int lastRow = mVisibleRowCount - 1;

            //Adjust to align the last position in the bottom-right
            mFirstVisiblePosition = Math.max(
                    lastVisiblePosition - lastColumn - (lastRow * getTotalColumnCount()), 0);

            childLeft = getHorizontalSpace() - (mDecoratedChildWidth * mVisibleColumnCount);
            childTop = getVerticalSpace() - (mDecoratedChildHeight * mVisibleRowCount);

            //Correct cases where shifting to the bottom-right overscrolls the top-left
            // This happens on data sets too small to scroll in a direction.
            if (getFirstVisibleRow() == 0) {
                childTop = Math.min(childTop, 0);
            }
            if (getFirstVisibleColumn() == 0) {
                childLeft = Math.min(childLeft, 0);
            }
        }
    }

    //Clear all attached views into the recycle bin
    detachAndScrapAttachedViews(recycler);

    //Fill the grid for the initial layout of views
    fillGrid(DIRECTION_NONE, childLeft, childTop, recycler);
}

我们根据有没有已经被 attach 的子视图来判断当前是一个新的布局还是一个更新操作。 如果是更新,我们根据第一个可见视图的 position(通过监测视图左上角是哪个子视图) 和当前 x/y 滑动的位移这些信息去执行新的 fillGrid(),同时保证左上角的 item 位置不变。


下面是一些需要特殊处理得情况:


onAdapterChanged()

这个方法提供了另一个重置布局的场所,设置新的 adapter 会触发这个事件 (在这,setAdapter会被再次调用)。 这个阶段你可以安全的返回一个与之前 adapter 完全不同的视图。所以, 示例中我们移除了所有当前视图(并没有回收它们)。

@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
    //Completely scrap the existing layout
    removeAllViews();
}

移除视图会触发一个新的布局过程,当 onLayoutChildren() 被再次调用时, 我们的代码会执行创建新视图的布局过程,因为现在没有 attched 的子视图。


Scroll to Position

另一个重要的特性就是给 LayoutManager 添加滚动到特定位置的功能。 可以带有有动画效果,也可以没有,下面是对应的两个回调方法。

scrollToPosition()

当 layout 应该立即将所给位置设为第一个可见 item 时,调用 RecyclerView 的 scrollToPosition()。 在一个 vertical list 里,item 应该放在顶部;horizontal list 中,通常放在左边。在我们的 网格布局中,被选中的 item 应该放在视图的左上角。

@Override
public void scrollToPosition(int position) {
    if (position >= getItemCount()) {
        Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
        return;
    }

    //Ignore current scroll offset, snap to top-left
    mForceClearOffsets = true;
    //Set requested position as first visible
    mFirstVisiblePosition = position;
    //Trigger a new view layout
    requestLayout();
}

因为有一个良好的 onLayoutChildren() 实现,这里就可以简单的更新目标位置并触发一个 新的 fill 操作。


smoothScrollToPosition()

在带有动画的情况下,我们需要使用一些稍微不同的方法。 在这方法里我们需要创建一个 RecyclerView.SmoothScroller实例, 然后在方法返回前请求startSmoothScroll()启动动画。

RecyclerView.SmoothScroller 是提供 API 的抽象类,含有四个方法:



你可以自己实现一个 scroller 达到你想要的效果。不过这里我们只使用系统提供的 LinearSmoothScroller 就好了。只需实现一个方法computeScrollVectorForPosition(), 然后告诉 scroller 初始方向还有从当前位置滚动到目标位置的大概距离。


@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) {
    if (position >= getItemCount()) {
        Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
        return;
    }

    /*
     * LinearSmoothScroller's default behavior is to scroll the contents until
     * the child is fully visible. It will snap to the top-left or bottom-right
     * of the parent depending on whether the direction of travel was positive
     * or negative.
     */
    LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
        /*
         * LinearSmoothScroller, at a minimum, just need to know the vector
         * (x/y distance) to travel in order to get from the current positioning
         * to the target.
         */
        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            final int rowOffset = getGlobalRowOfPosition(targetPosition)
                    - getGlobalRowOfPosition(mFirstVisiblePosition);
            final int columnOffset = getGlobalColumnOfPosition(targetPosition)
                    - getGlobalColumnOfPosition(mFirstVisiblePosition);

            return new PointF(columnOffset * mDecoratedChildWidth, rowOffset * mDecoratedChildHeight);
        }
    };
    scroller.setTargetPosition(position);
    startSmoothScroll(scroller);
}

在这个实现中,和现有 ListView 的行为相似,无论是 RecyclerView 的哪个方向滚动, 当视图完全可见时滚动就会停止。


接下来?

我们现在已经有些起色了!事实上还有很多可以实现的功能。在下篇文章中,我们会介绍 如何给你的 LayoutManager 提供数据集改变时的动画效果。


Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8