Android

理解RecyclerView LayoutManager(二)

2017-12-06  本文已影响66人  From64KB

在上一篇文章中,我们简单介绍了实现一个自定LayoutManager几个核心的方法。在着接下来这篇文章中我们将会让这个layout Manager支持其他一些必要的功能。

Supporting Item Decorations 支持Item Decorations

对于在子View上绘制一些自定义的内容或者在不改变子View layout parameter的情况下修改子View的margin等操作,RecyclerView提供了一个非常简洁的类: RecyclerView.ItemDecoration,来实现这些功能。其中后一个功能规定了子View应该如何布局是 LayoutManager必须实现的功能。

LayoutManager给我们提供了一些辅助的方法来帮助我们支持decorations:

只要采用了上面这些方法来获取View的属性和测量值,RecyclerView就会自动帮我们处理好Item Decorations。

Data Set Changes 数据集合发生改变

当已经attach的RecyclerView.Adapter通过notifyDataSetChanged()触发更新时,LayoutManager会负责更新这个View中的布局。在这种情况下onLayoutChildren()会被再次触发。为了支持这个功能,我们首先需要区分是完全创建一个新的布局还是由于Adapter更新导致的布局改变(怎样理解这句话?举个例子,创建一个新的布局就对应于填充一个还没有任何数据的RecyclerView,Adapter更新导致的布局改变对应于向已有数据的RecyclerView中填充数据。)。下面就是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 overscroll when shifting to the bottom-right
            // 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);
}

上面这段代码中,我们通过判断是否已经有子View被attach来确定这是完全创建一个新的布局还是由于Adapter更新导致的布局改变。在更新的情况下,第一个可见的位置和当前的x/y偏移量可以帮助我们添加新的View,同时保持同样的item仍然在top-left位置。

下面有两种情况是需要我们处理的:

onAdapterChanged()

在整个Adapter被换掉(例如:setAdapter()被调用)时,可以重新设置layout。在这种情况下,我们必须假设新的Adapter返回的View和之前的Adapter返回的完全不同,以免造成不必要的麻烦。所以我们直接移除所有的View而不是像之前一样回收这些View,代码如下:

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

移除这些View会触发一个新的layout传递过来,当onLayoutChildren() 方法被重新调用,将会产生一个新的layout,因为RecyclerView中没有其他任何的View。

Scroll to Position滚动到制定位置

另一个需要自定义LayoutManager提供的功能是支持滚动到指定的位置。这种滚动有支持和不支持动画效果两种方式,并且每种方式都有相应的回调。

scrollToPosition()

当这个方法被调用时,RecyclerView会立刻滚动到指定位置对应的Item。在一个垂直滚动的列表中,这个Item会被放置到顶部;在水平滚动的列表中,这个Item会被放置到左边。在我们grid的例子中,这个被选中的位置,会被放置到这个View的左上角。下面是代码:

@Override
public void scrollToPosition(int position) {
    if (position >= getItemCount()) {
        Log.e(TAG, "Cannot scroll to "+position+", item count "+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(),可以完成更新目标位置触发填充功能。

smoothScrollToPosition()

和上面scrollToPosition()方法不同点在于,这个方法触发的滚动是有动画的。因此我们需要采取一些不同的措施。实现这个功能要求我们在LayoutManager中构建一个RecyclerView.SmoothScroller实例,通过调用startSmoothScroll()方法开始相应的动画。

RecyclerView.SmoothScroller是由4个必须实现的方法组成的抽象类:

如果动画的时间过长(或者滚动的距离过小)framework就会发出警告。确保动画的时间符合framework的要求。

如果你需要微调滚动动画,那么就需要提供自定义的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 "+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.
     */
    final Context context = recyclerView.getContext();
    LinearSmoothScroller scroller = new LinearSmoothScroller(context) {
        /*
         * 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非常相似,当所有的子View都可见时,就会停止滚动。

Now What?这样就够了?

现在基本已经可以满足我们对于一个LayoutManager所需要的一般功能了。下面一篇文章将会介绍在data set改变时触发更新动画。

参考资料

上一篇 下一篇

猜你喜欢

热点阅读