RecyclerView真的是无所不能
/ 今日科技快讯 /
近日,据国外媒体报道,美国电商亚马逊公司告知平台卖家,从即日起至4月5日,亚马逊设在美国、英国以及其他欧洲地区仓库只接收生活必需品等重要物资,这是该公司为应对新型冠状病毒疫情蔓延而释放仓储空间的最新举措。
/ 作者简介 /
本篇文章来自琼珶和予的投稿,分享了RecyclerView中自定义LayoutManager及相关组件的源码分析,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
另外,之前我的公众号里也分享过另外一篇非常优秀的玩转LayoutManager的文章,感兴趣的朋友请查看文章底部的推荐阅读。祝大家周五愉快。
琼珶和予的博客地址:
https://www.jianshu.com/u/00b25c511dd3
/ 前言 /
对于使用ReccyclerView的我们来说,LayoutManager早已非常熟悉。可是,有没有想过我们所说的熟悉是哪种熟悉?对的,就是会使用而已,这其中包括谷歌爸爸帮我们实现的几种LayoutManager,例如:LinearLayoutManager,GridLayoutManager等等。
仔细想一想,我们使用LayoutManager就像我们当初初学Android时使用各种基础控件,我们处于只会使用的阶段,如果后续有一些特殊的要求,系统的实现已经不能满足我们自身的需求,此时自定义LayoutManager就必须出手了。同时,如果想要自定义LayoutManager,我们就必须了解它相关的原理。所以,学习LayoutManager的源码是至关重要的。
介于LayoutManger的特殊性,我们不可能将LayoutManager及其所有子类的代码都分析一遍,所以本文的源码分析重点是,从源码角度来解释为什么这样自定义LayoutManager。自定义LayoutManager要求的门槛相对较高,它不是简单的照着模板来写,而是需要了解它内部的原理,这其中包括回收机制(这个我们在分析RecyclerView的三大流程时已经从LinearLayoutManager内部看到了),滑动机制等等。所以,在自定义LayoutManager时,我默认大家都懂得这些原理,如果还有同学不懂的话,可以参考我的文章:
本文打算从如下几个角度来分析LayoutManager:
知识储备--相关方法的解释,这里的相关方法主要是自定义涉及到的方法
自定义一个LayoutManager
SnapHelper基本使用、源码分析和自定义SnapHelper
我们都知道LayoutManager就是一个布局管理器,主要负责RecyclerView的ItemView测量和布局,所以自定义LayoutManager的过程跟自定义View的过程非常的相似。本文打算从一个Demo开始来介绍怎么自定义一个LayoutManager,效果如下:
方法名 | 作用 |
重写generateDefaultLayoutParams方法
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
onLayoutChildren方法
定位每个ItemView的位置,然后布局。
适配滑动和缩放的效果。
然后我们可以设置一个offset,后面的ItemView根据这个offset来重新定位。我们通过之前看LinearLayoutManager源码的经验,发现LinearLayoutManager计算位置通过一个remainSpace变量来实现的。remainSpace表示当前RecyclerView的剩余空间,每布局一个ItemView,remainSpace减去小消耗的距离就OK!
下面我结合代码来具体分析:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.getItemCount() == 0 || state.isPreLayout()) return;
removeAndRecycleAllViews(recycler);
if (!mHasChild) {
mItemViewHeight = getVerticalSpace();
mItemViewWidth = (int) (mItemViewHeight / mItemHeightWidthRatio);
mHasChild = true;
}
mItemCount = getItemCount();
mScrollOffset = makeScrollOffsetWithinRange(mScrollOffset);
fill(recycler);
}
return Math.min(Math.max(mItemViewWidth, scrollOffset), mItemCount * mItemViewWidth);
}
在onLayoutChidlren方法的最后,调用fill方法。fill方法才是真正计算每个ItemView的位置,我们来看看:
// 1.初始化基本变量
int bottomVisiblePosition = mScrollOffset / mItemViewWidth;
final int bottomItemVisibleSize = mScrollOffset % mItemViewWidth;
final float offsetPercent = bottomItemVisibleSize * 1.0f / mItemViewWidth;
final int space = getHorizontalSpace();
int remainSpace = space;
final int defaultOffset = mItemViewWidth / 2;
final List<ItemViewInfo> itemViewInfos = new ArrayList<>();
// 2.计算每个ItemView的位置信息(left和scale)
for (int i = bottomVisiblePosition - 1, j = 1; i >= 0; i--, j++) {
double maxOffset = defaultOffset * Math.pow(mScale, j - 1);
int start = (int) (remainSpace - offsetPercent * maxOffset - mItemViewWidth);
ItemViewInfo info = new ItemViewInfo(start, (float) (Math.pow(mScale, j - 1) * (1 - offsetPercent * (1 - mScale))));
itemViewInfos.add(0, info);
remainSpace -= maxOffset;
if (remainSpace < 0) {
info.setLeft((int) (remainSpace + maxOffset - mItemViewWidth));
info.setScale((float) Math.pow(mScale, j - 1));
break;
}
}
// 3.添加最右边ItemView的相关信息
if (bottomVisiblePosition < mItemCount) {
final int left = space - bottomItemVisibleSize;
itemViewInfos.add(new ItemViewInfo(left, 1.0f));
} else {
bottomVisiblePosition -= 1;
}
// 4.回收其他位置的View
final int layoutCount = itemViewInfos.size();
final int startPosition = bottomVisiblePosition - (layoutCount - 1);
final int endPosition = bottomVisiblePosition;
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View childView = getChildAt(i);
final int position = convert2LayoutPosition(i);
if (position > endPosition || position < startPosition) {
detachAndScrapView(childView, recycler);
}
}
// 5.先回收再布局
detachAndScrapAttachedViews(recycler);
for (int i = 0; i < layoutCount; i++) {
fillChild(recycler.getViewForPosition(convert2AdapterPosition(startPosition + i)), itemViewInfos.get(i));
}
}
变量名 | 含义 |
当bottomVisiblePosition < mItemCoun时(没有大于的情况)时,也是在滑动的时,是在第三步时将最右边的ItemView的位置信息计算出来。
关于位置信息的计算,这里就不讨论了,都是一些常规的计算逻辑。
最后就是布局,调用的是fillChild方法:
addView(view);
measureChildWithExactlySize(view);
final int top = getPaddingTop();
layoutDecoratedWithMargins(view, itemViewInfo.getLeft(), top, itemViewInfo.getLeft() + mItemViewWidth, top + mItemViewHeight);
view.setScaleX(itemViewInfo.getScale());
view.setScaleY(itemViewInfo.getScale());
}
到这里onLayoutChildren方法算是重新完毕了,这个过程中,比较难以理解的是位置信息的计算,这个我也不知道怎么解释,大家就自己发挥想象力吧。
水平滑动
public boolean canScrollHorizontally() {
return true;
}
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
int pendingScrollOffset = mScrollOffset + dx;
mScrollOffset = makeScrollOffsetWithinRange(pendingScrollOffset);
fill(recycler);
return mScrollOffset - pendingScrollOffset + dx;
}
滑动之后最右边的ItemView都能完整显示
@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
mSnapHelper.attachToRecyclerView(view);
}
源码
还有不懂的同学可以我的github去下载源码:LayoutManagerDemo。特别感谢:LayoutManagerGroup,本文自定义的LayoutManager大部分思路和源码都来自于它。
通常来说,我们在日常开发中,使用RecyclerView很少遇到的SnapHelper,不过,如果你想要自定义LayoutManager来实现一些特殊效果,很大的可能性会遇到SnapHelper。那么SnapHelper到底是什么呢?是怎么使用的呢?它的实现原理又是什么呢?这是本文需要解答的三个问题。
SnapHelper的源码分析
方法名 | 返回类型 | 含义 |
findTargetSnapPosition:此方法表示fling滑动能滑到的位置。
calculateDistanceToFinalSnap和findSnapView:这两个方法表示正常滑动的能到达位置,其中calculateDistanceToFinalSnap表示距离,这个过程涉及到因为对齐操作而进行的距离重新调整;findSnapView方法表示正常滑动能到达的位置对应的ItemView。
attachToRecyclerView方法
throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();
}
}
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
OnScrollListener
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
if (mRecyclerView == null) {
return;
}
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
OnFlingListener
public boolean onFling(int velocityX, int velocityY) {
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter == null) {
return false;
}
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
所以SnapHelper是否处理fling事件,还需要看它的snapFromFling方法。我们来看看:
int velocityY) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
}
SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
整个SnapHelper的原理就是这样,非常的简单,接下来我们结合实际来看看怎么自定义一个SnapHelper。
自定义SnapHelper
@Override
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
if (layoutManager instanceof CustomLayoutManger) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = ((CustomLayoutManger) layoutManager).calculateDistanceToPosition(
layoutManager.getPosition(targetView));
out[1] = 0;
} else {
out[0] = 0;
out[1] = ((CustomLayoutManger) layoutManager).calculateDistanceToPosition(
layoutManager.getPosition(targetView));
}
return out;
}
return null;
}
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
return RecyclerView.NO_POSITION;
}
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof CustomLayoutManger) {
int pos = ((CustomLayoutManger) layoutManager).getFixedScrollPosition();
if (pos != RecyclerView.NO_POSITION) {
return layoutManager.findViewByPosition(pos);
}
}
return null;
}
}
重写generateDefaultLayoutParams方法;
重写onLayoutChildren方法,对ItemView进行布局;
处理滑动,例如水平滑动需要重写canScrollHorizontally和scrollHorizontallyBy;
如果需要处理对齐问题,可以使用SnapHelper。
https://github.com/BeauteousJade/LayoutManagerDemo