美文网首页
自定义LayoutManager之GreedoLayoutMan

自定义LayoutManager之GreedoLayoutMan

作者: 恨水东逝 | 来源:发表于2018-06-13 14:49 被阅读0次

    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<View> 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<Double> 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之GreedoLayoutMan

          本文链接:https://www.haomeiwen.com/subject/dasjeftx.html