RecyclerView 源码学习:一步一步自定义LayoutManager
Posted 自在时刻
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecyclerView 源码学习:一步一步自定义LayoutManager相关的知识,希望对你有一定的参考价值。
前言
这篇文章实现了一个简单的LayoutManager,重点在于从原理方面一步一步去了解如何自定义一个LayoutManager。麻雀虽小,但五脏俱全。从自定义LayoutManager的layout布局,到scroll滑动,再到添加简单的动画效果。 其实,自定义一个LayoutManager也没那么难。
基本概念
Recycler
LayoutManafger调用 getViewForPosition 获取一个item,Recycler会决定是从缓存返回还是生成新的item。在自定义LayoutManger的时候,要保证不可见的视图被传递给Recycler。
Scrap 或 Recycler
Recycler是二级缓存,一个scrap heap 和一个 recyle pool, scrap 中的数据是正确的数据,比如我们快速上下滑动列表时,在边缘的栏目一会显示一会消失,所以会放在scrap中。 而已经消失并不使用的item,会被放在recyle中,其中的数据也是不正确的。
每次LayoutManager去请求一个视图调用getViewForPosition的时候,会先从scrap heap中找,存在直接返回。否则去recyle pool 中找一个视图,然后重新在adapter中绑定数据。最终如果还没有缓存,调用我们在adapter中重写的onCreateViewHolder,生成一个新的ViewHolder绑定数据并返回。
使用 detachAndScrapView 将视图放进scrap中去,使用removeAndRecycleView 将可能不会再用的视图放回recycler并且后续如果使用,还要进行rebind
小结
其实,上面这些都是废话,只要知道要获得一个view和用完一个view,都要通过recycler。常用的方法有getViewForPosition ,detachAndScrapView 和 removeAndRecycleView
自定义LayoutManager
generateDefaultLayoutParams
作用:控制每个item的layoutParams
为每一个childView设置的LayoutParams在这个方法中返回。很简单,一般我们都直接返回一个WrapContent的lp
初始布局 onLayoutChildren
这个方法会在一个view 第一次执行layout的时候调用,同时也会在adaper的数据集改变并通知观察者(也就是view)的时候调用。所以在其中每一次布局的时候,要先将之前放置的无用的View放回recycler中,因为这些View我们在后续还可能使用,为了减少初始化以及bind的时间,我们调用detachAndScrapAttachedViews。此外,对于不会再用到的View,可以调用removeAndRecycleView进行回收。
if (getItemCount() == 0)
offset = 0;
detachAndScrapAttachedViews(recycler);
return;
这里自定义的LayoutManager比较简单,假定全部的item都是相同的大小。所以可以在一开始进行测绘:
if (getChildCount() == 0)
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
startLeft = (getHorizontalSpace() - mDecoratedChildWidth) / 2;
startTop = (getVerticalSpace() - mDecoratedChildHeight) / 2;
interval = 10;
detachAndScrapView(scrap, recycler);
这里注意getItemCount和getChildCount的区别:前者是adapter中添加的数据的数目,而后者是当前recyclerView中已经添加的子View的数目。所以上述代码的含义就是,如果没有添加过子View,那么从recycler中取出一个并完成测绘:
recycler.getViewForPosition(0);
addView(scrap);
测绘完成后,再重新放回recycler中,调用
detachAndScrapView(scrap, recycler);
最后,再将之前添加的全部子View放回recycler中,因为一会还要使用,为了避免rebind,调用
detachAndScrapAttachedViews(recycler);
然后就可以进行layoutChildren的过程了。
先来一个简单的,如下:
int left = 100, top = 0;
for (int i = 0; i< getItemCount(); i++)
if (outOfRange(top)) continue;
View scrap = recycler.getViewForPosition(i);
measureChildWithMargins(scrap, 0, 0);
addView(scrap);
layoutDecorated(scrap, left, top, left + mDecoratedChildWidth, top + mDecoratedChildHeight);
top += mDecoratedChildHeight + interval;
基本效果就是这样:
处理滑动 canScroll 和 scrollXXXBy
基本的布局有了之后,就可以处理滑动了。
RecyclerView是一个ViewGroup,如果要处理滑动事件,必然要进行拦截,分析其中的onInterceptTouchEvent方法:
关键代码如下:
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
...
case MotionEvent.ACTION_MOVE:
...
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop)
mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
startScroll = true;
if (canScrollVertically && Math.abs(dy) > mTouchSlop)
mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
startScroll = true;
...
if (startScroll) setScrollState(SCROLL_STATE_DRAGGING);
...
return mScrollState == SCROLL_STATE_DRAGGING;
这里可以知道,如果要拦截某个方向的滑动事件,那么要在mLayout也就是LayoutManager中重写相应的canScrollxxx方法。
比如我们要允许竖直方向的滑动,直接重写如下:
@Override
public boolean canScrollVertically()
return true;
再来看一下事件拦截以后,在onTouchEvent中怎么处理的:
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev))
getParent().requestDisallowInterceptTouchEvent(true);
代码很多,关键在于在ACTION_MOVE事件中调用了scrollByInternal方法,其中又有如下方法:
if (x != 0)
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
if (y != 0)
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
就是调用了LayoutManager中自定义的scrollxxxBy方法,并且传入Recycler供我们获取和回收View,以及相应的坐标x和y。
除此之外,要注意这个scrollxxxBy方法还有个返回值,这个返回值就是我们当前处理了的滑动坐标。如果这个值小于传入的坐标,表明我们已经滑动到了尽头,这么说可能有点抽象,举个例子:
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)
return 0;
返回0的时候,无论怎样都会小于传入的dy,看一下效果:
可以看到,我向下和向上滑动的时候,上边沿和下边沿都会出现一个动画效果,表明已经到头了!就是由于返回值是0的缘故。
分析到这里,基本可以确定如何添加滚动效果了,关键在两点:
- canScrollXXX中返回true
- 在onTouchEvent中scrollXXXBy方法不断被调用,在其中完成LayoutChildren不断对子View进行放置,从而形成动画效果。
为了完成第二个目的,我们需要在代码中添加一些额外的属性,主要就是每个item的偏移量,这样,在获得dy的时候,可以在每个item原有偏移量的基础上进行移动以及回收不需要的view。
首先,用一个全局变量 List offsetList 来存储每一个item的偏移量,并在onLayoutChildren中进行初始化:
for (int i = 0; i < getItemCount(); i++)
offsetList.add(property);
property += mDecoratedChildHeight + interval;
滑动方面的方法:
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)
int willScroll = dy;
offset += willScroll;
layoutItems(recycler, state, dy);
return willScroll;
并且,原先对item的layout的过程也要进行一些修改:
private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int dy)
for (int i = 0; i < getChildCount(); i++)
View view = getChildAt(i);
int pos = getPosition(view);
if (outOfRange(offsetList.get(pos) - offset))
removeAndRecycleView(view, recycler);
detachAndScrapAttachedViews(recycler);
int left = 100;
for (int i = 0; i< getItemCount(); i++)
int top = offsetList.get(i);
if (outOfRange(top - offset)) continue;
View scrap = recycler.getViewForPosition(i);
measureChildWithMargins(scrap, 0, 0);
if (dy >= 0)
addView(scrap);
else
addView(scrap, 0);
layoutDecorated(scrap, left, top - offset, left + mDecoratedChildWidth, top - offset + mDecoratedChildHeight);
上述代码中,对每一个item记录了一下它的位置,然后滑动过程中offset+=dy,并且每次滑动后都出发LayoutItems方法,并且每个item在初始化y值的基础上减去offset,得到新的布局的位置。
到此为止,就有了滑动的动画效果:
缩放效果
经常有这样一种需求,当滑动列表的时候,列表中间部分某些item会呈现出放大之类的动画效果。其实,这种效果的实现其实就是通过item的属性动画。
实现的思路也比较简单,定一条基准线middle如下:
在每一个进行layout的时候计算每一个item的坐标,距离middle中线最近的那个我们给它放大,就实现了一个类似选中当前重点的效果。当然,具体的动画效果我们可以自己去计算选择。
新的layoutItems代码如下:
private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int dy)
for (int i = 0; i < getChildCount(); i++)
View view = getChildAt(i);
int pos = getPosition(view);
if (outOfRange(offsetList.get(pos) - offset))
removeAndRecycleView(view, recycler);
detachAndScrapAttachedViews(recycler);
int left = 100;
View selectedView = null;
float maxScale = Float.MIN_VALUE;
for (int i = 0; i< getItemCount(); i++)
int top = offsetList.get(i);
if (outOfRange(top - offset)) continue;
View scrap = recycler.getViewForPosition(i);
measureChildWithMargins(scrap, 0, 0);
if (dy >= 0)
addView(scrap);
else
addView(scrap, 0);
int deltaY = Math.abs(top - offset - middle);
scrap.setScaleX(1);
scrap.setScaleY(1);
float scale = 1 + (mDecoratedChildHeight / (deltaY + 1));
if (scale > maxScale)
maxScale = scale;
selectedView = scrap;
layoutDecorated(scrap, left, top - offset, left + mDecoratedChildWidth, top - offset + mDecoratedChildHeight);
if (selectedView != null)
maxScale = maxScale > 2 ? 2 : maxScale;
selectedView.setScaleX(maxScale);
selectedView.setScaleY(maxScale);
最后可以得到下面这样一个比较粗糙的效果:
最后,来贴一下完整的代码吧:
package rouchuan.circlelayoutmanager;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Created by yangyang on 2017/3/13.
*/
public class SimpleLayoutManager extends RecyclerView.LayoutManager
private int mDecoratedChildWidth;
private int mDecoratedChildHeight;
private int interval;
private int middle;
private int offset;
private List<Integer> offsetList;
public SimpleLayoutManager(Context context)
offsetList = new ArrayList<>();
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams()
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
if (getItemCount() == 0)
offset = 0;
detachAndScrapAttachedViews(recycler);
return;
//初始化的过程,还没有childView,先取出一个测绘。 认为每个item的大小是一样的
if (getChildCount() == 0)
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
interval = 10;
middle = (getVerticalSpace() - mDecoratedChildHeight) / 2;
detachAndScrapView(scrap, recycler);
//回收全部attach 的 view 到 recycler 并重新排列
int property = 0;
for (int i = 0; i < getItemCount(); i++)
offsetList.add(property);
property += mDecoratedChildHeight + interval;
detachAndScrapAttachedViews(recycler);
layoutItems(recycler, state, 0);
@Override
public boolean canScrollVertically()
return true;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)
int willScroll = dy;
offset += willScroll;
if (offset < 0 || offset > offsetList.get(offsetList.size() - 1)) return 0;
layoutItems(recycler, state, dy);
return willScroll;
private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int dy)
for (int i = 0; i < getChildCount(); i++)
View view = getChildAt(i);
int pos = getPosition(view);
if (outOfRange(offsetList.get(pos) - offset))
removeAndRecycleView(view, recycler);
detachAndScrapAttachedViews(recycler);
int left = 100;
View selectedView = null;
float maxScale = Float.MIN_VALUE;
for (int i = 0; i< getItemCount(); i++)
int top = offsetList.get(i);
if (outOfRange(top - offset)) continue;
View scrap = recycler.getViewForPosition(i);
measureChildWithMargins(scrap, 0, 0);
if (dy >= 0)
addView(scrap);
else
addView(scrap, 0);
int deltaY = Math.abs(top - offset - middle);
scrap.setScaleX(1);
scrap.setScaleY(1);
float scale = 1 + (mDecoratedChildHeight / (deltaY + 1));
if (scale > maxScale)
maxScale = scale;
selectedView = scrap;
layoutDecorated(scrap, left, top - offset, left + mDecoratedChildWidth, top - offset + mDecoratedChildHeight);
if (selectedView != null)
maxScale = maxScale > 2 ? 2 : maxScale;
selectedView.setScaleX(maxScale);
selectedView.setScaleY(maxScale);
private boolean outOfRange(float targetOffSet)
return targetOffSet > getVerticalSpace() + mDecoratedChildHeight ||
targetOffSet < -mDecoratedChildHeight;
private int getHorizontalSpace()
return getWidth() - getPaddingLeft() - getPaddingRight();
private int getVerticalSpace()
return getHeight() - getPaddingTop() - getPaddingBottom();
以上是关于RecyclerView 源码学习:一步一步自定义LayoutManager的主要内容,如果未能解决你的问题,请参考以下文章
(原创)[C#] 一步一步自定义拖拽(Drag&Drop)时的鼠标效果:基本原理及基本实现
Android一步一步带你实现RecyclerView的拖拽和侧滑删除功能