自定义LayoutManager之GreedoLayoutManager

RecyclerView非常灵活,支持用户自定义布局,本文简单分析下500px的代码,有助于将来实现自己的LayoutManager。
GreedoLayoutManager

两个主要的类

布局类,继承RecyclerView.LayoutManager,实现真正的功能,

    public GreedoLayoutManager(SizeCalculatorDelegate sizeCalculatorDelegate) {
        mSizeCalculator = new GreedoLayoutSizeCalculator(sizeCalculatorDelegate);
    }

尺寸计算类,负责计算每个ITEM的大小,在LayoutManager初始化时一并初始化,

    public GreedoLayoutSizeCalculator(SizeCalculatorDelegate sizeCalculatorDelegate) {
        //adatper的代理,用于在adapter中取得图片的宽高比
        mSizeCalculatorDelegate = sizeCalculatorDelegate;
        //存放每个ITEM的size,size是自定义类,里面只有宽高两个变量
        mSizeForChildAtPosition = new ArrayList<>();
        //存放每行ITEM中第一个ITEM的位置
        mFirstChildPositionForRow = new ArrayList<>();
        //存放每个ITEM对应的行数
        mRowForChildPosition = new ArrayList<>();
    }

简单分析实现流程

  1. 需要实现自己的LayoutParams
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        );
    }
  1. 初始化时调用或者adapter数据变化时调用,初始化时默认从左上角开始布局,如果是adapter change导致数据变化,需要根据mForceClearOffsets来判断是否需要保留当前的ITEM偏移量。
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;
        }

        mSizeCalculator.setContentWidth(getContentWidth());
        mSizeCalculator.reset();

        int initialTopOffset = 0;
        if (getChildCount() == 0) { // First or empty layout
            mFirstVisiblePosition = 0;
            mFirstVisibleRow = 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) {
                initialTopOffset = 0;
                mForceClearOffsets = false;
            } else {
                initialTopOffset = getDecoratedTop(topChild);
            }
        }
        //回收当前全部ITEM
        detachAndScrapAttachedViews(recycler);
        //网格化布局填充
        preFillGrid(Direction.NONE, 0, initialTopOffset, recycler, state);
        mPendingScrollPositionOffset = 0;
    }
  1. 布局代码,
  • 首先通过firstChildPositionForRow获取第一个可视ITEM的位置,后面要根据这个ITEM开始布局。
  • 如果第一个可视ITEM发生了变化,说明TOP端有一行上滑隐藏了或者下滑显示了。此时需要重新获取当前viewgroup中第一个child的显示偏移值,以便重新计算startTopOffset。
  • 接下来将当前显示的全部ITEM缓存,然后detach掉,这里不是回收。detachView是非常轻量级的操作。
  • while循环布局每一个ITEM,直到铺满屏幕或者处理完全部ITEM等
    这里先从缓存中读取需要布局位置的ITEM,如果没有在通过recycler获取一个新的。根据当前ITEM的宽度,计算再几个ITEM之后需要换行,更新好对应的偏移量,分别是leftOffset 和topOffset 。若是缓存中获取到的ITEM,说明此ITEM滑动前也是在屏幕中显示的,所以不需要重新BIND数据,attachView就好了,同时将它从缓存中移出。若是recycler获取的ITEM,说明是从屏幕外滑进来的新ITEM,需要addView到Viewgroup中,并重新测量和布局。
    分别是measureChildWithMargins和layoutDecorated。
  • 最后,如果当前缓存还有ITEM,说明是滑动后被移出屏幕了,需要全部回收掉。recycler.recycleView
private int preFillGrid(Direction direction, int dy, int emptyTop,
                            RecyclerView.Recycler recycler, RecyclerView.State state) {
        int newFirstVisiblePosition = firstChildPositionForRow(mFirstVisibleRow);

        // First, detach all existing views from the layout. detachView() is a lightweight operation
        //      that we can use to quickly reorder views without a full add/remove.
        SparseArray viewCache = new SparseArray<>(getChildCount());
        int startLeftOffset = getPaddingLeft();
        int startTopOffset  = getPaddingTop() + emptyTop;

        if (getChildCount() != 0) {
            startTopOffset = getDecoratedTop(getChildAt(0));
            if (mFirstVisiblePosition != newFirstVisiblePosition) {
                switch (direction) {
                    case UP: // new row above may be shown
                        double previousTopRowHeight = sizeForChildAtPosition(
                                mFirstVisiblePosition - 1).getHeight();
                        startTopOffset -= previousTopRowHeight;
                        break;
                    case DOWN: // row may have gone off screen
                        double topRowHeight = sizeForChildAtPosition(
                                mFirstVisiblePosition).getHeight();
                        startTopOffset += topRowHeight;
                        break;
                }
            }

            // Cache all views by their existing position, before updating counts
            for (int i = 0; i < getChildCount(); i++) {
                int position = mFirstVisiblePosition + i;
                final View child = getChildAt(i);
                viewCache.put(position, child);
            }

            // Temporarily detach all cached views. Views we still need will be added back at the proper index
            for (int i = 0; i < viewCache.size(); i++) {
                final View cachedView = viewCache.valueAt(i);
                detachView(cachedView);
            }
        }

        mFirstVisiblePosition = newFirstVisiblePosition;

        // Next, supply the grid of items that are deemed visible. If they were previously there,
        //      they will simply be re-attached. New views that must be created are obtained from
        //      the Recycler and added.
        int leftOffset = startLeftOffset;
        int topOffset  = startTopOffset + mPendingScrollPositionOffset;
        int nextPosition = mFirstVisiblePosition;

        int currentRow = 0;

        while (nextPosition >= 0 && nextPosition < state.getItemCount()) {

            boolean isViewCached = true;
            View view = viewCache.get(nextPosition);
            if (view == null) {
                view = recycler.getViewForPosition(nextPosition);
                isViewCached = false;
            }

            if (mIsFirstViewHeader && nextPosition == HEADER_POSITION) {
                measureChildWithMargins(view, 0, 0);
                mHeaderViewSize = new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
            }

            // Overflow to next row if we don't fit
            Size viewSize = sizeForChildAtPosition(nextPosition);
            if ((leftOffset + viewSize.getWidth()) > getContentWidth()) {
                // Break if the rows limit has been hit
                if (currentRow + 1 == mRowsLimit) break;
                currentRow++;

                leftOffset = startLeftOffset;
                Size previousViewSize = sizeForChildAtPosition(nextPosition - 1);
                topOffset += previousViewSize.getHeight();
            }

            // These next children would no longer be visible, stop here
            boolean isAtEndOfContent;
            switch (direction) {
                case DOWN: isAtEndOfContent = topOffset >= getContentHeight() + dy; break;
                default:   isAtEndOfContent = topOffset >= getContentHeight();      break;
            }
            if (isAtEndOfContent) break;

            if (isViewCached) {
                // Re-attach the cached view at its new index
                attachView(view);
                viewCache.remove(nextPosition);
            } else {
                addView(view);
                measureChildWithMargins(view, 0, 0);

                int right  = leftOffset + viewSize.getWidth();
                int bottom = topOffset  + viewSize.getHeight();
                layoutDecorated(view, leftOffset, topOffset, right, bottom);
            }

            leftOffset += viewSize.getWidth();

            nextPosition++;
        }

        // Scrap and store views that were not re-attached (no longer visible).
        for (int i = 0; i < viewCache.size(); i++) {
            final View removingView = viewCache.valueAt(i);
            recycler.recycleView(removingView);
        }

        // Calculate pixels laid out during fill
        int pixelsFilled = 0;
        if (getChildCount() > 0) {
            pixelsFilled = getChildAt(getChildCount() - 1).getBottom();
        }

        return pixelsFilled;
    }
  1. 滑动支持,允许垂直滑动
    public boolean canScrollVertically() {
        return true;
    }
  1. 滑动处理
  • 首先根据滑动方向及滑动距离,判断是否要重新布局。比如有ITEM滑出屏幕或者滑入屏幕。这会影响到第一个可视ITEM的位置变化,mFirstVisibleRow。
  • 前面已经处理完滑动后的重新布局,这里仅仅需要整体移动下全部视图即可。offsetChildrenVertical(-scrolled);
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }

        final View topLeftView = getChildAt(0);
        final View bottomRightView = getChildAt(getChildCount() - 1);
        int pixelsFilled = getContentHeight();
        // TODO: Split into methods, or a switch case?
        if (dy > 0) {
            boolean isLastChildVisible = (mFirstVisiblePosition + getChildCount()) >= getItemCount();

            if (isLastChildVisible) {
                // Is at end of content
                pixelsFilled = Math.max(getDecoratedBottom(bottomRightView) - getContentHeight(), 0);

            } else if (getDecoratedBottom(topLeftView) - dy <= 0) {
                // Top row went offscreen
                mFirstVisibleRow++;
                pixelsFilled = preFillGrid(Direction.DOWN, Math.abs(dy), 0, recycler, state);

            } else if (getDecoratedBottom(bottomRightView) - dy < getContentHeight()) {
                // New bottom row came on screen
                pixelsFilled = preFillGrid(Direction.DOWN, Math.abs(dy), 0, recycler, state);
            }
        } else {
            if (mFirstVisibleRow == 0 && getDecoratedTop(topLeftView) - dy >= 0) {
                // Is scrolled to top
                pixelsFilled = -getDecoratedTop(topLeftView);

            } else if (getDecoratedTop(topLeftView) - dy >= 0) {
                // New top row came on screen
                mFirstVisibleRow--;
                pixelsFilled = preFillGrid(Direction.UP, Math.abs(dy), 0, recycler, state);

            } else if (getDecoratedTop(bottomRightView) - dy > getContentHeight()) {
                // Bottom row went offscreen
                pixelsFilled = preFillGrid(Direction.UP, Math.abs(dy), 0, recycler, state);
            }
        }

        final int scrolled = Math.abs(dy) > pixelsFilled ? (int) Math.signum(dy) * pixelsFilled : dy;
        offsetChildrenVertical(-scrolled);

        // Return value determines if a boundary has been reached (for edge effects and flings). If
        //      returned value does not match original delta (passed in), RecyclerView will draw an
        //      edge effect.
        return scrolled;
    }
  1. ITEM尺寸的获取(宽和高)
  • 在获取每行第一个显示ITEM时(firstChildPositionForRow),需要先计算好每个ITEM的宽高(computeChildSizesUpToPosition),才能得到一行能显示多少个。
  • ITEM的高可以设置为固定(setFixedHeight),也可以只设最大高度动态判断(setMaxRowHeight)。动态判断是根据adapter中得到的每个图片的宽高比(aspectRatioForIndex)综合计算出来的。
  • 高度计算,先用屏幕宽度和本行第一个ITEM的宽高比计算宽和高,如果高度超过最大高度,则加上第二个ITEM的比例,直到高度小于最大高度。确定好高度后则可以确定每个ITEM的宽度,最后一个ITEM的宽度是屏幕剩余宽度。
private void computeChildSizesUpToPosition(int lastPosition) {
        if (mContentWidth == INVALID_CONTENT_WIDTH) {
            throw new RuntimeException("Invalid content width. Did you forget to set it?");
        }

        if (mSizeCalculatorDelegate == null) {
            throw new RuntimeException("Size calculator delegate is missing. Did you forget to set it?");
        }

        int firstUncomputedChildPosition = mSizeForChildAtPosition.size();
        int row = mRowForChildPosition.size() > 0
                ? mRowForChildPosition.get(mRowForChildPosition.size() - 1) + 1 : 0;

        double currentRowAspectRatio = 0.0;
        List itemAspectRatios = new ArrayList<>();
        int currentRowHeight = mIsFixedHeight ? mMaxRowHeight : Integer.MAX_VALUE;

        int currentRowWidth = 0;
        int pos = firstUncomputedChildPosition;
        while (pos <= lastPosition || (mIsFixedHeight ? currentRowWidth <= mContentWidth : currentRowHeight > mMaxRowHeight)) {
            double posAspectRatio = mSizeCalculatorDelegate.aspectRatioForIndex(pos);
            currentRowAspectRatio += posAspectRatio;
            itemAspectRatios.add(posAspectRatio);

            currentRowWidth = calculateWidth(currentRowHeight, currentRowAspectRatio);
            if (!mIsFixedHeight) {
                currentRowHeight = calculateHeight(mContentWidth, currentRowAspectRatio);
            }

            boolean isRowFull = mIsFixedHeight ? currentRowWidth > mContentWidth : currentRowHeight <= mMaxRowHeight;
            if (isRowFull) {
                int rowChildCount = itemAspectRatios.size();
                mFirstChildPositionForRow.add(pos - rowChildCount + 1);

                int[] itemSlacks = new int[rowChildCount];
                if (mIsFixedHeight) {
                    itemSlacks = distributeRowSlack(currentRowWidth, rowChildCount, itemAspectRatios);

                    if (!hasValidItemSlacks(itemSlacks, itemAspectRatios)) {
                        int lastItemWidth = calculateWidth(currentRowHeight,
                                itemAspectRatios.get(itemAspectRatios.size() - 1));
                        currentRowWidth -= lastItemWidth;
                        rowChildCount -= 1;
                        itemAspectRatios.remove(itemAspectRatios.size() - 1);

                        itemSlacks = distributeRowSlack(currentRowWidth, rowChildCount, itemAspectRatios);
                    }
                }

                int availableSpace = mContentWidth;
                for (int i = 0; i < rowChildCount; i++) {
                    int itemWidth = calculateWidth(currentRowHeight, itemAspectRatios.get(i)) - itemSlacks[i];
                    itemWidth = Math.min(availableSpace, itemWidth);

                    mSizeForChildAtPosition.add(new Size(itemWidth, currentRowHeight));
                    mRowForChildPosition.add(row);

                    availableSpace -= itemWidth;
                }

                itemAspectRatios.clear();
                currentRowAspectRatio = 0.0;
                row++;
            }

            pos++;
        }
    }
  1. 500px还支持了scrollToPosition函数
    public void scrollToPosition(int position) {
        if (position >= getItemCount()) {
            Log.w(TAG, String.format("Cannot scroll to %d, item count is %d", position, getItemCount()));
            return;
        }

        // Scrolling can only be performed once the layout knows its own sizing
        // so defer the scrolling request after the postLayout pass
        if (mSizeCalculator.getContentWidth() <= 0) {
            mPendingScrollPosition = position;
            return;
        }

        mForceClearOffsets = true; // Ignore current scroll offset
        mFirstVisibleRow = rowForChildPosition(position);
        mFirstVisiblePosition = firstChildPositionForRow(mFirstVisibleRow);

        requestLayout();
    }

小结

500px实现的layoutmanager简洁明了,滑动流畅,基本可以根据它来实现想要的各种自定义layout效果。

你可能感兴趣的:(自定义LayoutManager之GreedoLayoutManager)