注*以下都是来自Google官方的自定义LayoutManager文档,在学习的过程中整理出来的。
原文地址是:http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
错误的地方还请指出谢谢啦。
第一节内容:https://blog.csdn.net/qq_43447999/article/details/97674999
Supporting Item Decorations
RecyclerView有一个非常简洁的特性。ItemDecoration实例可以与子视图内容一起进行自定义绘图,还可以提供应用于子视图的insets(边距),而不需要修改布局参数.
LayoutManager为我们提供了帮助方法.
- 要获取子视图的左边缘,使用getDecoratedLeft()而不是child. getLeft ().
- 要获得子视图的顶部边缘,使用getDecoratedTop()而不是child. getTop ().
- 要获得子视图的右边缘,使用getDecoratedRight()而不是child. getRight ().
- 要获取子视图的底部边缘,使用getDecoratedBottom()而不是child. getBottom ().
- 用measurereChild()或measurereChildWithMargin()而不是child.measure()衡量来自Recycler的view.
- 使用layoutDecoration()而不是child.layout()布局来自Recycler的新view
- 使用getDecoratedMeasuredWidth() 或者getDecoratedMeasuredHeight()
而不是child.getMeasuredWidth() 或者 child.getMeasuredHeight()来获取子视图的度量值。
Data Set Changes
当有新增View的时候,Adapter通过notifyDataSetChanged()触发更新,LayoutManager将负责更新视图中的布局。本例中,将再次调用onLayoutChildren()。为了支持这一点,我们需要对示例进行一些调整,以区分新布局和由于适配器更新而导致的布局更改。下面是来自FixedGridLayoutManager的完整方法:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
RecyclerView.State state) {
//清除所有现有视图
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
//在空布局上测量子view的数值
if (getChildCount() == 0) {
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
//因为视图大小相同所以需要预先计算下列值。
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
detachAndScrapView(scrap, recycler);//再回收
}
updateWindowSizing();
int childLeft;
int childTop;
if (getChildCount() == 0) {
//重置可见和滚动位置
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
} else if (getVisibleChildCount() > getItemCount()) { //数据太少就不能滚动
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
} else { //数据集发生变化
// 保存现有的初始位置和滚动的偏移量
final View topChild = getChildAt(0);
if (mForceClearOffsets) {
childLeft = childTop = 0;
mForceClearOffsets = false;
} else {
childLeft = getDecoratedLeft(topChild);
childTop = getDecoratedTop(topChild);
}
//在新布局中如果超出界限则调整滚动的位置
int lastVisiblePosition = positionOfIndex(getVisibleChildCount() - 1);
if (lastVisiblePosition >= getItemCount()) {
lastVisiblePosition = (getItemCount() - 1);
int lastColumn = mVisibleColumnCount - 1;
int lastRow = mVisibleRowCount - 1;
//调整到右下角的最后一个位置
mFirstVisiblePosition = Math.max(lastVisiblePosition
- lastColumn - (lastRow * getTotalColumnCount()), 0);
childLeft = getHorizontalSpace()
- (mDecoratedChildWidth * mVisibleColumnCount);
childTop = getVerticalSpace()
- (mDecoratedChildHeight * mVisibleRowCount);
if (getFirstVisibleRow() == 0) {
childTop = Math.min(childTop, 0);
}
if (getFirstVisibleColumn() == 0) {
childLeft = Math.min(childLeft, 0);
}
}
}
detachAndScrapAttachedViews(recycler);//回收
//重新布局
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler);}
我们根据是否已经附加了子视图来确定这是一个新的布局还是一个更新。在更新的情况下,第一个可见位置(即持续跟踪的左上角视图)和当前滚动的x/y偏移量为我们提供了足够的信息来执行一个新的fillGrid(),同时保持相同的项位置仍然位于左上角。
还处理一些特殊情况。
- 当新数据集太小而无法滚动时,布局将重置为左上角的位置0。
- 如果新数据集更小,并且保留当前位置将导致布局超出边界(在右侧和/或底部)。这里我们调整第一个位置,使布局与网格的右下角对齐。(这两点的区别可以参考如下)
The first point means that the data set is small enough for all the views to fit on screen… no scrolling is allowed in any direction.
The second is a bit more complicated. Since the logic attempts to keep the same item position in the top-left corner, a significant enough reduction in data set might cause the grid to shrink to the point
where laying out the remaining tiles with the same position in the
top-left would overscroll the grid to have whitespace on the right
and/or bottom. The correction for this is to offset the grid to be
exactly aligned with the right and/or bottom of the new grid at the
expense of maintaining the top-left position.
onAdapterChanged()
这个方法为您提供了一个额外的机会来重置布局,以防整个适配器被换出(即在视图上再次调用setAdapter())。在这种情况下,假设返回的视图与之前的适配器完全不同是比较安全的。因此,我们的例子只是删除所有当前视图(不回收它们):
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
RecyclerView.Adapter newAdapter) {
//移除所有的view
removeAllViews();}
视图移除的时候就会再次触发一次新的布局传递,会再次调用onLayoutChildren(),这样我们的代码可以执行一个新的布局,因为不再附加任何子视图。
Scroll to Position
scrollToPosition() 这个方法能够告诉视图滚动到指定位置。这可以在有动画或没有动画的情况下完成,每个动画都有一个回调。
当布局应该立即使用给定的位置作为第一个可见项进行更新时,就会被调用。
在垂直列表中,元素将放在顶部;在水平列表中,它通常位于左侧。在我们的网格中,“selected”位置将位于视图的左上角。
@Override
public void scrollToPosition(int position) {
if (position >= getItemCount()) {
Log.e(TAG, "Cannot scroll to "+position+", item count "+getItemCount());
return;
}
//忽略当前滚动偏移量,对齐到左上角
mForceClearOffsets = true;
//设置请求位置为第一个可见的Posistion
mFirstVisiblePosition = position;
//出发一个新的布局视图
requestLayout();}
正确使用onLayoutChildren()只需更新目标位置并触发新的填充即可。
smoothScrollToPosition()
此方法的契约是让LayoutManager()构造RecyclerView.SmoothScroller的实例,并在方法返回之前调用startSmoothScroll()开始动画。
RecyclerView.SmoothScroller是一个抽象类,它的API包含四个必需的方法:
- onStart():当滚动块动画开始时触发。
- onStop():当滚动块动画结束时触发。
- onSeekTargetStep():当滚动块搜索目标视图时递增地调用。该放过发负责读取所提供的dx/dy,并更新视图实际应该在两个方向滚动的距离。RecyclerView.SmoothScroller.Action实例被传递给这个方法。通过向操作的update()方法传递一个新的dx、dy、duration和Interpolator,通知视图下一个增量应该如何动画。
如果你的动画制作时间过长(即你的增量太小),该框架会提醒你;尝试调整动画步骤以匹配框架中的标准动画持续时间。
- onTargetFound(): 在附加了目标位置的视图之后,只调用一次。这是将目标视图动画到其确切位置的最后一次机会。-在内部,它使用LayoutManager中的findViewByPosition()来确定何时附加视图。如果LayoutManager能够有效地将视图映射到位置,则重写此方法以提高性能。默认的实现会遍历所有子视图…
如果真的想微调滚动动画,可以提供自己的滚动器实现。我们选择使用LinearSmoothScroller,它为我们实现回调工作。我们只需要实现一个方法computeScrollVectorForPosition()来告诉滚动块从当前位置到目标位置所需的初始方向和大致距离。
@Override
public void smoothScrollToPosition(RecyclerView recyclerView,
RecyclerView.State state,
final int position) {
if (position >= getItemCount()) {
Log.e(TAG, "Cannot scroll to "+position+", item count "+getItemCount());
return;
}
// LinearSmoothScroller的默认行为是滚动内容,直到子级完全可见。
//它将捕捉到父对象的左上角或右下角,具体取决于移动方向是正还是负。
final Context context = recyclerView.getContext();
LinearSmoothScroller scroller = new LinearSmoothScroller(context) {
//LinearSmoothScroller 只需要知道要移动的向量(x/y)的距离,以便从当前位置移动到目标位置.
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
final int rowOffset = getGlobalRowOfPosition(targetPosition)
- getGlobalRowOfPosition(mFirstVisiblePosition);
final int columnOffset = getGlobalColumnOfPosition(targetPosition)
- getGlobalColumnOfPosition(mFirstVisiblePosition);
return new PointF(columnOffset * mDecoratedChildWidth,
rowOffset * mDecoratedChildHeight);
}
};
scroller.setTargetPosition(position);
startSmoothScroll(scroller);}
这个实现与ListView的行为类似,在视图完全可见时将停止滚动;不管它是在RecyclerView的左边、上面、右边还是底部。