RecyclerView(三):LayoutManager职责及相关方法

概述

在RecyclerView之前,对于线性布局和网格布局用的基本上是ListView和GridView,到RecyclerView,就不需要这么麻烦了,RecyclerView对于职责划分的很明确,布局相关的就只需要LayoutManager,继承LayoutManager就可以实现你想要的布局,比如android为我们提供的一下几个布局:

  1. 线性布局:LinearLayoutManager
  2. 网格布局:GridLayoutManager
  3. 瀑布流布局:StaggerGridLayoutManager

这些布局管理都直接或间接实现了LayoutManager,这也就是LayoutManager的主要作用:布局管理。
LayoutManager是RecyclerView的内部类,RecyclerView把它的测量和布局工作都转交给了LayoutManager,如果你想要的布局android没有给你提供,那么就可以考虑去继承LayoutManager去实现。

这几个布局提供的常见方法
  • findFirstCompletelyVisibleItemPosition()
  • findFirstVisibleItemPosition()
  • findLastCompletelyVisibleItemPosition()
  • findLastVisibleItemPosition()
  • onSaveInstanceState()
  • onRestoreInstanceState()

前面四个方法好理解,看名字应该就能知道,找到最前最后显示item的位置,其中含有Completely的方法返回的是完全可见的第一个item,而不含的则是真正的第一个item,这个item可能只显示了一部分,这个显示需要注意一点,比如使用的LinearLayoutManager布局,这个可见指的是布局方向上的可见。
onSaveInstanceState():
对于onSaveInstanceState()这个方法肯定很多人都见过,那就是在Activity中就有这个方法,当Activity异常销毁(比如由于内存过大)时就会调用到这个方法,而在LayoutManager中提供的这个方法是一个空方法,在他的子类中提供了实现,这里看下在LinearLayoutManager中的实现:

    @Override
    public Parcelable onSaveInstanceState() {
        if (mPendingSavedState != null) {
            return new SavedState(mPendingSavedState);
        }
        SavedState state = new SavedState();
        // RecyclerView中是否含有子view,如果有就会保存子view的状态
        if (getChildCount() > 0) {
            ensureLayoutState();
            boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
            state.mAnchorLayoutFromEnd = didLayoutFromEnd;
            //判断布局是否是倒序
            if (didLayoutFromEnd) {
            // 倒序布局
                final View refChild = getChildClosestToEnd();
                state.mAnchorOffset = mOrientationHelper.getEndAfterPadding()
                        - mOrientationHelper.getDecoratedEnd(refChild);
                state.mAnchorPosition = getPosition(refChild);
            } else {
            // 顺序布局,可以看到,state中主要是保存两个变量,一个是当前显示第一个view所在的position,另一个是当前这个
            // view没有显示的偏移量
                final View refChild = getChildClosestToStart();
                state.mAnchorPosition = getPosition(refChild);
                state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild)
                        - mOrientationHelper.getStartAfterPadding();
            }
        } else {
            state.invalidateAnchor();
        }
        return state;
    }

这里可以看出主要是保存了两个变量,保存的这两个变量有什么用呢?主要是在RecyclerView被remove后再次添加进来时可以恢复上一次显示item的位置,什么意思呢?先来看下下面这个界面:
在这里插入图片描述
可以看到这个布局的最外层是一个RecyclerView,里面的子item还有RecyclerView,当我们往上滑的时候,子RecyclerView最终是会被reMove()掉的,子RecyclerView中的子item的显示的位置信息是不会被记录的,当再次往回滑的时候,子RecyclerView显示的总是会是第一个,如果想往回滑的时候,子RecyclerView显示的总是上次滑动到的位置,那么要怎么去是想呢?这时候就可以用到上面提到的onSaveInstanceState(),在这个方法里正好会保存RecyclerView的位置信息,当我们往回滑添加的之前被回收的RecyclerView时,那就可以将上次保存的位置信息取出来再次添加进去就可以了,思路是有了,接下来就是如何在代码中去实现了,首先是回收的时候保存信息,关于item的回收,想到的就是Adapter的onViewRecycled()方法,接下来就是去恢复它的位置信息了,我想到的就是onBindViewHolder()中可以去恢复它的位置信息,毕竟reMove()的item都会调用到这个方法(detach掉的view本身就会保留它本身的信息,不需要我们维护),下面是一些实现的关键代码:
1.确定当前的item当中是否含有RecyclerView:

    RecyclerView findNestedRecyclerView(@NonNull View view) {
        if (!(view instanceof ViewGroup)) {
            return null;
        }
        if (view instanceof RecyclerView) {
            return (RecyclerView) view;
        }
        final ViewGroup parent = (ViewGroup) view;
        final int count = parent.getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView descendant = findNestedRecyclerView(child);
            if (descendant != null) {
                return descendant;
            }
        }
        return null;
    }

2.回收时保存当前RecyclerView中item的位置信息:onViewRecycled()

		private SparseArray<Parcelable> rvState = new SparseArray<>();
        @Override
        public void onViewRecycled(@NonNull MyViewHolder holder) {
            super.onViewRecycled(holder);
            RecyclerView nestedRecyclerView = findNestedRecyclerView(holder.itemView);
            if (nestedRecyclerView != null) {
                Parcelable parcelable = nestedRecyclerView.getLayoutManager().onSaveInstanceState();
                // 保存时key为当前item所处的位置position,值为子RecyclerView的子item的显示位置信息
                rvState.put(holder.getAdapterPosition(),parcelable);
            }
        }

3.重新绑定时恢复RecyclerView中item的位置信息,onBindViewHolder():

        @Override
        public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) {
            Log.d(TAG, "onBindViewHolder: position = "+i);
            // 这里type=0的item的布局中就有RecyclerView
            if (getItemViewType(i) == 0) {
                myViewHolder.rv.setLayoutManager(new LinearLayoutManager(TestRVActivity.this,RecyclerView.HORIZONTAL,false));
                // 获取当前position之前回收时的位置信息,如果不为null,那么就恢复之前的位置信息
                Parcelable parcelable = rvState.get(i);
                if (parcelable != null) {
                    rvState.remove(i);
                    // 
                    myViewHolder.rv.getLayoutManager().onRestoreInstanceState(parcelable);
                }
                if (myViewHolder.rv.getAdapter() == null) {
                    myViewHolder.rv.setAdapter(new MyChildAdapter());
                }
                return;
            }
        }
自定义LayoutManager

如果需要实现自定义布局,那么可以实现generateDefaultLayoutParams()和onLayoutChildren(),第二个方法主要就是给他进行布局,能不能滑动主要由canScrollHorizontally()和canScrollVertically()进行决定,返回true表示可以滑动,当然,滑动也是需要我们进行处理的,scrollHorizontallyBy()和scrollVerticallyBy()就是让我们来处理滑动的,可以在这两个方法中调用offsetChildrenHorizontal()进行滑动处理。这里看下我写的一个小demo,可以好好理解下:

public class CustomeLayoutManager extends RecyclerView.LayoutManager {
    private static final String TAG = "CustomeLayoutManager";
    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;

    @IntDef({VERTICAL, HORIZONTAL})
    public @interface OrientationType {
    }

    @OrientationType
    private int mOrientation;
    private int mRows;
    private int mColms;
    private int mOnePageSize;
    private int mMoveDistance;
    private int mMaxScrollX;
    private int mMaxScrollY;
    private SparseArray<Rect> mItemFramsList = new SparseArray<>();

    public CustomeLayoutManager(@IntRange(from = 1, to = 30) int row, @IntRange(from = 1, to = 30) int col, @OrientationType int orientation) {
        this.mRows = row;
        this.mColms = col;
        mOnePageSize = mRows * mColms;
        this.mOrientation = orientation;
    }


    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    @Override
    public boolean canScrollHorizontally() {
        return mOrientation == HORIZONTAL;
    }

    @Override
    public boolean canScrollVertically() {
        return mOrientation == VERTICAL;
    }

    private int getWidthUse() {
        int width = getWidth() - getPaddingLeft() - getPaddingRight();
        return width;
    }

    private int getHeightUse() {
        return getHeight() - getPaddingTop() - getPaddingBottom();
    }

    private int getItemWidth() {
        return getWidthUse() / mColms;
    }

    private int getItemHeight() {
        return getHeightUse() / mRows;
    }

    private Rect getItemFrameByPosition(int position) {
        Rect rect = mItemFramsList.get(position);
        if (rect == null) {
            rect = new Rect();
            int currentPage = position / mOnePageSize;
            //当前页编译量
            int offsetX = 0;
            int offsetY = 0;
            if (canScrollHorizontally()) {
                offsetX = getWidthUse() * currentPage;
            } else {
                offsetY = getHeightUse() * currentPage;
            }
            int itemIndex = position % mOnePageSize;
            int rowIndex = itemIndex / mColms;
            int colIndex = itemIndex % mColms;
            offsetX += colIndex * getItemWidth();
            offsetY += rowIndex * getItemHeight();
            rect.left = offsetX;
            rect.top = offsetY;
            rect.right = offsetX + getItemWidth();
            rect.bottom = offsetY + getItemHeight();
        }
        return rect;
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        int i1 = getItemCount();

        int pageCount = getItemCount() / mOnePageSize;
        if (getItemCount() % mOnePageSize == 0) {
            pageCount--;
        }
        Log.d(TAG, "onLayoutChildren: i1 = " + i1+"   pageCount = "+pageCount+"    width = "+getWidthUse());
        if (canScrollHorizontally()) {
            mMaxScrollX = getWidthUse() * pageCount;
        } else {
            mMaxScrollY = getHeightUse() * pageCount;
        }

        for (int i = 0; i < i1; i++) {
            View childView = recycler.getViewForPosition(i);
            addView(childView);
            Rect rect = getItemFrameByPosition(i);
            measureChildWithMargins(childView, 0, 0);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) childView.getLayoutParams();
            layoutDecorated(childView, rect.left + params.leftMargin, rect.top + params.topMargin,
                    rect.right - params.rightMargin, rect.bottom - params.bottomMargin);
        }
    }

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        Log.d(TAG, "scrollHorizontallyBy: mMoveDistance = " + mMoveDistance + "     " + dx+"   mMaxScrollX = "+mMaxScrollX);
        if (mMoveDistance + dx < 0) {
            dx = 0 - mMoveDistance;
        }
        if (mMoveDistance + dx > mMaxScrollX) {
            dx = mMaxScrollX - mMoveDistance;
        }
        mMoveDistance += dx;
        offsetChildrenHorizontal(-dx);
        return dx;
    }
    
}

就写到这了,如果还想更深入的学习,可以自己花点时间在研究下源码。如果有不懂的欢迎一起讨论学习。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值