【记录】记录点滴
【需求】Recycler需要特殊排列顺序时,要实现自定义LayoutManager
自定义大致分为三步:1. 放置全部的View;2. 滑动;3. 回收机制
1. RecyclerView继承自ViewGroup,每个 item 就是它的子 view,重新设置子 view的放置位置,就需要重写onLayout。LayoutManager中提供了 onLayoutChildren(),它负责对子 view 布局。
RecyclerView中的源码
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//...
dispatchLayout();
//...
}
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
eatRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
从dispatchLayoutStep2的注释中看出,它是真正实现布局(放置子view)的地方,它的内部就调用了LayoutManager的onLayoutChildren。
1)继承RecyclerView.LayoutManager,重写 generateDefaultLayoutParams 和 onLayoutChildren
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
//detach全部子View,并放入缓存
detachAndScrapAttachedViews(recycler);
layoutChildren(recycler);
}
private void layoutChildren(RecyclerView.Recycler recycler){
//每个子 view 的 top 位置
int childTop = 0;
for(int i = 0; i < getItemCount(); i++){
//最终调用tryGetViewHolderForPositionByDeadline,要么使用已有的ViewHolder,要么直接创建一个
View childView = recycler.getViewForPosition(i);
addView(childView);
//add后,计算子 view 的宽高
measureChildWithMargins(childView, 0, 0);
//宽和高附带了分割线的尺寸,这里没有使用分割线
int width = getDecoratedMeasuredWidth(childView);
int height = getDecoratedMeasuredHeight(childView);
//最终调用 view.layoutfan 方法
layoutDecorated(childView, 0, childTop, width, childTop + height);
childTop += height;
}
}
到这里,至少界面可以展示了。补充下,上述代码在测量view和布局时调用方法不是很严谨,方法如下
//测量时,具体还涉及 getChildMeasureSpec 方法的处理逻辑,只简单描述下是否考虑margin
//测量时,考虑了margin
measureChildWithMargins(childView, 0, 0);
//测量时,不考虑margin
measureChild(childView, 0, 0);
//布局时,考虑margin
layoutDecoratedWithMargins(childView, 0, childTop, width, childTop + height);
//布局时,不考虑margin
layoutDecorated(childView, 0, childTop, width, childTop + height);
2. 滑动,涉及到下面4个方法
//能否横向滑动,返回true,表示可以滑动
canScrollHorizontally()
//能否纵向滑动,返回true,表示可以滑动
canScrollVertically()
//横向滑动的距离,返回int,用于边缘与fling效果
int scrollHorizontallyBy()
//纵向滑动的距离,返回int,用于边缘与fling效果
int scrollVerticallyBy()
一个垂直滑动的示例
@Override
public boolean canScrollVertically() {
return true;
}
int verticalScrollOffset = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//列表向下滑动dy>0
//否则dy<0
int distance = dy;
if (verticalScrollOffset + dy < 0) {//如果滑动的偏移量<0
distance = -verticalScrollOffset;
} else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {//如果滑动后的偏移量超过到最大的偏移量
distance = totalHeight - getVerticalSpace() - verticalScrollOffset;
}
//将竖直方向的偏移量+distance
verticalScrollOffset += distance ;
// 调用该方法通知view在y方向上移动指定距离
offsetChildrenVertical(-distance);
return distance;
}
private int getVerticalSpace() {
//计算RecyclerView的可用高度,减去上下Padding值
return getHeight() - getPaddingBottom() - getPaddingTop();
}
private int getHorizontalSpace() {
//计算RecyclerView的可用高度,减去上下Padding值
return getHeight() - getPaddingBottom() - getPaddingTop();
}
到这里,就可以实现滑动了。
3. 上面两步实现了布局和滑动,但这时所有的View都在,没有真正利用上回收功能。思路很简单,计算每个 item 的(l,t,r,b)位置信息,滑动时,判断 item 是否在展示区域内,如果已滑出区域则回收,否则继续展示。
1)先修改onLayoutChildren,计算每个 item 的位置信息,所以修改layoutChildren方法
SparseArray allItems = new SparseArray<>();
//这里稍微做点修改
//layoutChildern实际只做 item 的测量和 保存位置的工作
private void layoutChildren(RecyclerView.Recycler recycler){
int childTop = 0;
for(int i = 0; i < getItemCount(); i++){
View childView = recycler.getViewForPosition(i);
addView(childView);
measureChildWithMargins(childView, 0, 0);
int width = getDecoratedMeasuredWidth(childView);
int height = getDecoratedMeasuredHeight(childView);
//测量后就detach掉
detachAndScrapView(childView, recycler);
allItems.put(i, new Rect(0, childTop, width, childTop + height));
childTop += height;
totalHeight += height;
}
//循环中调用了detachAndScrapView,也可以最后调用detachAndScrapAttachedViews
//detachAndScrapAttachedViews(recycler);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
layoutChildren(recycler);
// recycleAndFillView 方法根据展示区域和各 item 位置坐标决定是否展示
recycleAndFillView(recycler, state);
}
再看看 recycleAndFillView方法
private void recycleAndFillView(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
// 当前scroll offset状态下的显示区域
Rect displayRect= new Rect(0, verticalScrollOffset, getHorizontalSpace(),
verticalScrollOffset + getVerticalSpace());
//重新显示需要出现在屏幕的子View
for (int i = 0; i < getItemCount(); i++) {
//判断ItemView的位置和当前显示区域是否重合
if (Rect.intersects(displayRect, allItems.get(i))) {
//获得Recycler中缓存的View
View itemView = recycler.getViewForPosition(i);
measureChildWithMargins(itemView, 0, 0);
//添加View到RecyclerView上
addView(itemView);
//取出先前存好的ItemView的位置矩形
Rect rect = allItems.get(i);
//将这个item布局出来
layoutDecoratedWithMargins(itemView,
rect.left,
rect.top - verticalScrollOffset, //因为现在是复用View,所以想要显示在
rect.right,
rect.bottom - verticalScrollOffset);
}
}
//展示实际渲染的 item 的个数
Log.e("lxy", "item count = " + getChildCount());
}
这时就基本实现了回收,再考虑下滑动的情况。
int verticalScrollOffset = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int distance = dy;
// recycleAndFillView 中循环判断时,可能重复添加同一个item,所以addview前要先detach
detachAndScrapAttachedViews(recycler);
if (verticalScrollOffset + dy < 0) {
distance = -verticalScrollOffset;
} else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {
distance = totalHeight - getVerticalSpace() - verticalScrollOffset;
}
//将竖直方向的偏移量+distance
verticalScrollOffset += distance;
// 调用该方法通知view在y方向上移动指定距离
offsetChildrenVertical(-distance);
recycleAndFillView(recycler, state);
return distance;
}
PS:滑动基于offsetChildrenVertical,也就是offsetXXXAndXXX方法,其他的博客说方法不会触发重新绘制,是基于RenderNode实现的。
offsetTopAndBottom
方法设置View的位置, 不会造成View的重绘,代码里可以看见通过RenderNode.offsetTopAndBottom
来实现的。(Draw绘制完的东西保存在RenderNode里面,所以位置的修改可以直接修改RenderNode)
参考
https://blog.csdn.net/huachao1001/article/details/51594004#rd
https://www.jianshu.com/p/7bb7556bbe10
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0517/2880.html