对于使用ReccyclerView
的咱们来讲,LayoutManager
早已很是熟悉。但是,有没有想过咱们所说的熟悉是哪一种熟悉?对的,就是会使用而已,这其中包括谷歌爸爸帮咱们实现的几种LayoutManager
,例如:LinearLayoutManager
,GridLayoutManager
等等。git
仔细想想,咱们使用LayoutManager
就像咱们当初初学Android时使用各类基础控件,咱们处于只会使用的阶段,若是后续有一些特殊的要求,系统的实现已经不能知足咱们自身的需求,此时自定义LayoutManager
就必须出手了。同时,若是想要自定义LayoutManager
,咱们就必须了解它相关的原理。因此,学习LayoutManager
的源码是相当重要的。github
本文参考资料:数组
介于LayoutManger
的特殊性,咱们不可能将LayoutManager
及其全部子类的代码都分析一遍,因此本文的源码分析重点是,从源码角度来解释为何这样自定义LayoutManager
。自定义LayoutManager
要求的门槛相对较高,它不是简单的照着模板来写,而是须要了解它内部的原理,这其中包括回收机制(这个咱们在分析RecyclerView
的三大流程时已经从LinearLayoutManager
内部看到了),滑动机制等等。因此,在自定义LayoutManager
时,我默认你们都懂得这些原理,若是还有同窗不懂的话,能够参考个人文章:缓存
本文打算从以下几个角度来分析LayoutManager
:bash
- 知识储备--相关方法的解释,这里的相关方法主要是自定义涉及到的方法
- 自定义一个
LayoutManager
SnapHelper
基本使用、源码分析和自定义SnapHelper
在正式分析LayoutManager
以前,咱们先来对LayoutManager
及其它的相关组件作一个简单的概述。ide
咱们都知道LayoutManager
就是一个布局管理器,主要负责RecyclerView
的ItemView
测量和布局,因此自定义LayoutManager
的过程跟自定义View
的过程很是的类似。本文打算从一个Demo开始来介绍怎么自定义一个LayoutManager
,效果以下: 源码分析
LayoutManager
相关的两个组件--
SnapHelper
和
SmoothScroller
。这个其中
SnapHelper
主要负责来调整
RecyclerView
的滑动距离,好比想要在滑动结束以后,
ItemView
停留在
RecyclerView
正中央,能够依靠
SnapHelper
。
咱们在自定义LayoutManager
以前,先来看一下LayoutManager
的几个方法。布局
方法名 | 做用 |
---|---|
generateDefaultLayoutParams | 抽象方法,必须实现。这个方法的做用主要是给RecyclerView 的ItemView 生成LayoutParams |
onMeasure | 用来测量RecyclerView 的大小的。一般不用重写此方法,可是在一种状况下必须重写,就是LayouytManager 不支持自动测量,这种状况下RecyclerView 不会进行自我测量,会调用LayoutManager 的onMeasure 方法来测量。 |
onLayoutChildren | 此方法的做用是布局ItemView 。此方法就像是ViewGroup 的onLayout 方法,RecyclerView 内部的ItemView 怎么布局,全看这个方法怎么实现。 |
canScrollHorizontally | 设置该LayoutManager 的RecyclerView 是否能够水平滑动。与之对应的还有canScrollVertically ,用来设置RecyclerView 是否垂直滑动 |
scrollHorizontallyBy | 水平能够滑动的距离。此方法带一个dx参数,表示RecyclerView 已经产生了dx 的滑动距离,此时咱们须要作的是调用相关方法,进行从新布局。同时此方法的返回值表示水平能够滑动的距离。与之对应的方法是scrollVerticallyBy 。 |
简单的了解了自定义LayoutManager
的几个方法,如今我将带领来实现一个Demo,具体的效果就是上面的gif动图,咱们来看看怎么本身实现一个LayoutMananger
。学习
首先,自定义LayoutManager
的第一步就是重写generateDefaultLayoutParams
方法,这个方法的做用在上面我已经介绍了,在这里就不介绍了。一般来讲,咱们这样来实现generateDefaultLayoutParams
方法就好了:优化
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
复制代码
咱们这里没有特殊的要求,因此让每一个ItemView
的自适应就好了。
而后,第二步就是重写onLayoutChildren
方法,也是最复杂的一步。在这一步,咱们主要完成两步:
- 定位每一个ItemView的位置,而后布局。
- 适配滑动和缩放的效果。
咱们先来结合图片来分析一下这个效果。
ItemView
是从左往右开始布局,不过咱们得从从右往左计算每一个
ItemView
的宽高,由于最右边的
ItemView
宽高是最原始,同时它的left位置也是最容易的计算(
RecyclerView
的水平空闲空间减去
ItemView
的
width
就行。)。
而后咱们能够设置一个offset
,后面的ItemView
根据这个offset来从新定位。咱们经过以前看LinearLayoutManager
源码的经验,发现LinearLayoutManager
计算位置经过一个remainSpace
变量来实现的。remainSpace
表示当前RecyclerView
的剩余空间,每布局一个ItemView
,remainSpace
减去小消耗的距离就OK!
下面我结合代码来具体分析:
@Override
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);
}
复制代码
在onLayoutChildren
方法里面,咱们初始化了几个变量,其中mItemViewHeight
和mItemViewWidth
两个变量分别表示ItemView
的高和宽。其次就是mScrollOffset
的初始化:
private int makeScrollOffsetWithinRange(int scrollOffset) {
return Math.min(Math.max(mItemViewWidth, scrollOffset), mItemCount * mItemViewWidth);
}
复制代码
第一次调用onLayoutChildren
方法来初始化mScrollOffset
时,mScrollOfffet
的值被设置为mItemCount * mItemViewWidth
。这有什么意义呢?我待会会解释。
在onLayoutChidlren
方法的最后,调用fill
方法。fill
方法才是真正计算每一个ItemView
的位置,咱们来看看:
private void fill(RecyclerView.Recycler recycler) {
// 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 | 表示此时RecyclerView 最右边能看见的ItemView 的position 。例如说,初始状况下,bottomVisiblePosition 就等于ItemCount ,固然此时bottomVisiblePosition 的结果确定是不对的,后面在使用时会根据状况来调整。 |
bottomItemVisibleSize | 这个变量没有特殊意义,主要的用来计算offsetPercent |
offsetPercent | 滑动的百分比,从1.0f~0.0f变化。 |
defaultOffset | 每一个ItemView 偏移的值(默认全部的ItemView 都是左对齐) |
而后就是计算每一个ItemView
的位置了。这里须要注意一个问题,就是bottomVisiblePosition == mItemCount
的状况。
当bottomVisiblePosition == mItemCount时,也是最初的状态,这种状况下,第二步就是直接将最右边的ItemView
的位置信息计算出来。
当bottomVisiblePosition < mItemCoun时(没有大于的状况)时,也是在滑动的时,是在第三步时将最右边的ItemView
的位置信息计算出来。
关于位置信息的计算,这里就不讨论了,都是一些常规的计算逻辑。
最后就是布局,调用的是fillChild
方法:
private void fillChild(View view, ItemViewInfo itemViewInfo) {
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());
}
复制代码
fillChild
方法没有解释的必要,熟悉自定义View
的同窗应该都懂。
到这里onLayoutChildren
方法算是从新完毕了,这个过程当中,比较难以理解的是位置信息的计算,这个我也不知道怎么解释,你们就本身发挥想象力吧。
接下来就是让RecyclerView
支持水平滑动。要想支持水平滑动,咱们必须重写canScrollHorizontally
方法和scrollHorizontallyBy
方法,咱们来看看:
@Override
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;
}
复制代码
这个过程当中,须要特别注意的是scrollHorizontallyBy
方法,咱们不能直接让mScrollOffset
加上dx
,由于mScrollOffset
的范围在[mItemViewWidth,mItemCount * mItemViewWidth]
,因此在每次滑动以后须要调整,得再一次调用makeScrollOffsetWithinRange
方法。
这个需求就很是的简单,自我实现一个SnaHelper
,而后这样使用就OK了:
private final SnapHelper mSnapHelper = new CustomSnapHelper();
@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
mSnapHelper.attachToRecyclerView(view);
}
复制代码
这里面具体的含义这里先不解释,待会在分析SnaHelper
时会详细的解释。
整个LayoutManager
的自定义过程就OK了,具体的效果就是上面的动图效果。
还有不懂的同窗能够个人github去下载源码:LayoutManagerDemo。特别感谢:LayoutManagerGroup,本文自定义的LayoutManager大部分思路和源码都来至于它。
SnaHelper
的存在对于RecyclerView
来讲,可谓是如虎添翼。SnaHelper
可见帮助咱们实现一些特殊的效果,好比说,咱们可使用RecyclerView
和SnapHelper
去实现ViewPager
的效果。
一般来讲,咱们在平常开发中,使用RecyclerView
不多遇到的SnapHelper
,不过,若是你想要自定义LayoutManager
来实现一些特殊效果,很大的可能性会遇到SnapHelper
。那么SnapHelper
究竟是什么呢?是怎么使用的呢?它的实现原理又是什么呢?这是本文须要解答的三个问题。
简单来讲,SnapHelper
就是一个Helper类,只是它的内部有两个监听接口:OnFlingListener
和OnScrollListener
,分别用来监听RecyclerView
的scroll事件和fling事件。
而SnapHelper
的使用也是很是的简单,就是在LayoutManager
的onAttachedToWindow
方法调用SnapHelper
的attachToRecyclerView
方法便可。咱们就从attachToRecyclerView
方法为入口来分析SnapHelper
的源码。
SnapHelper
的原理其实是很是的简单,你们不要惧怕。咱们在分析SnapHelper
源码以前,先来了解SnapHelper
几个比较重要的方法:
方法名 | 返回类型 | 含义 |
---|---|---|
calculateDistanceToFinalSnap | int[] | 计算RecyclerView 最终滑动的距离。返回的是一个长度为2的数组,其中0位置表示水平滑动的滑动距离,1位置表示垂直滑动的距离。 |
findTargetSnapPosition | int | 这个方法表示fling操做最终能滑动到I的temView的position。这个position称为targetSnapPosition ,位置上对应的View 就是targetSnapView 。若是找不到position,就返回RecyclerView.NO_POSITION |
findSnapView | View | 最终滑动位置对应的ItemView |
在这里,咱们必须区分一下findTargetSnapPosition
方法和calculateDistanceToFinalSnap
、findSnapView
方法的区别。
- findTargetSnapPosition:此方法表示fling滑动能滑到的位置。
- calculateDistanceToFinalSnap和findSnapView:这两个方法表示正常滑动的能到达位置,其中
calculateDistanceToFinalSnap
表示距离,这个过程涉及到由于对齐操做而进行的距离从新调整;findSnapView
方法表示正常滑动能到达的位置对应的ItemView
。
因此,咱们在自定义SnapHelper
时,为了简单起见,不能够处理fling操做,也就是findTargetSnapPosition
返回为RecyclerView.NO_POSITION
,而后让RecyclerView
本身进行fling
,等待滑动结束以后,会回调咱们的calculateDistanceToFinalSnap
和findSnapView
来进行位置对齐。这样作的好处就是,咱们不用既考虑fling又考虑普通滑动。
准备的差很少了,接下来咱们正式分析SnapHelper
的源码。咱们来看看attachToRecyclerView
方法:
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
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();
}
}
复制代码
attachToRecyclerView
很是的简单,就是设置给RecyclerView
设置了两个监听接口:
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
复制代码
而后RecyclerView
开心的滑动,就会回调到咱们的两个监听事件里面来。
咱们先来看看OnScrollListener
接口的实现,看看它作了哪些事情:
private final RecyclerView.OnScrollListener mScrollListener =
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;
}
}
};
复制代码
咱们发现,当RecyclerView
滑动结束以后,就会调用snapToTargetExistingView
方法。那snapToTargetExistingView
方法是干吗的呢?其实就是保证对齐的。咱们来看看:
void snapToTargetExistingView() {
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]);
}
}
复制代码
咱们发现,在这里先是调用了findSnapView
方法找到滑动的最终ItemView
,而后根据找到的SnapView
,调用calculateDistanceToFinalSnap
方法来计算滑动的距离,最后调用相关方法来进行对齐。整个过程就是这么的简单。
SnapHelper
内部自己没有一个OnFingListener
接口对象,而是自身实现了OnFingListener
,因此当RecyclerView
在fling时,会回调此onFling
方法。咱们来看看:
@Override
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);
}
复制代码
首先,咱们要明白一个东西,若是RecyclerView
有一个OnFlingListener
处理fling事件的话,那么RecyclerView
就不会再处理fling事件。
因此SnapHelper
是否处理fling事件,还须要看它的snapFromFling
方法。咱们来看看:
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
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;
}
复制代码
在上面的代码中,咱们发现,findTargetSnapPosition
若是返回为RecyclerView.NO_POSITION
,那么SnapHelper
就不会处理fling事件。而若是SnapHelper
要处理fling事件的话,会经过LayoutManager
的startSmoothScroll
方法。这里面的原理实际上仍是调用到RecyclerView
的ViewFlinger
里面去了。
整个SnapHelper
的原理就是这样,很是的简单,接下来咱们结合实际来看看怎么自定义一个SnapHelper
。
一般来讲,咱们自定义SnapHelper
,实现三个抽象方法就已经差很少,分别是calculateDistanceToFinalSnap
方法、findTargetSnapPosition
方法和findSnapView
方法就已经够了。我么来看看咱们本身实现的CustomSnapHelper
:
public class CustomSnapHelper extends 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;
}
}
复制代码
方法的具体含义我这里就再也不解释了,你们能够个人Demo项目和上面对三个方法的解释来进行理解,总之来讲,SnapHelper
仍是比较简单的。
到这里,咱们对LayoutManager
相关分析就差很少,在最后,我作一个小小的总结。
- 自定义LayoutManager须要注意四点:1.重写
generateDefaultLayoutParams
方法;2.重写onLayoutChildren
方法,对ItemView
进行布局;3. 处理滑动,例如水平滑动须要重写canScrollHorizontally
和scrollHorizontallyBy
;4. 若是须要处理对齐问题,可使用SnapHelper
。- 自定义
SnapHelper
咱们只须要重写它的三个抽象方法便可,分别是:calculateDistanceToFinalSnap
、findTargetSnapPosition
和findSnapView
。须要注意的是,为了简单起见,咱们能够直接在findTargetSnapPosition
内部返回RecyclerView.NO_POSITION
,让RecyclerView
来帮助咱们处理fling事件。
若是不出意外的话,接下来我将分析ItemAnimator
。