前言 只要有坚强的持久心,一个庸俗平凡的人也会有成功的一天,否则即使是一个才识卓越的人,也只能遭遇失败的命运。 -----比尔盖茨
系列文章: Android自定义控件三部曲文章索引: http://blog.csdn.net/harvic880925/article/details/50995268
在上篇中,我们先将摆好所有要显示的新增item以后,再使用offsetChildrenVertical(-travel)
函数来移动屏幕中所有item。很明显,这种方法仅适用于每个item,在移动时,没有特殊效果的情况,当我们在移动item时,同时需要改变item的角度、透明度等情况时,单纯使用offsetChildrenVertical(-travel)
来移是不行的。针对这种情况,我们就只有使用第二种方法来实现回收复用了。
在本节中,我们最终实现的效果如下图所示:
从效果图中可以看出,本例中的每个item,在移动时,同时会绕Y轴旋转。
因为大部分的原理与上节中的CustomLayoutManager的实现相同,所以本节中的代码将从4.4中的CustomLayoutManager中改造而成。
一、 初步实现
1.1 实现原理
在这里,我们主要替换掉在上节中移动item所用的offsetChildrenVertical(-travel);
函数,既然要将它弃用,那我们就只能自己布局每个item了。很明显,在这里我们主要处理的是滚动的情况,对于onLayoutChildren
中的代码是不用改动的。
试想,在滚动dy时,有两种item需要重新布局:
- 第一种:原来已经在屏幕上的item
- 第二种:新增的item
所以,这里就涉及到怎么处理已经在屏幕上的item和新增item的重绘问题,我们可以效仿在onLayoutChildren
中的处理方式,先调用detachAndScrapAttachedViews(recycler)
将屏幕上已经在显示的所有Item离屏,然后再将所有item重绘。
那第二个问题又来了,我们应该从哪个item开始重绘,到哪个item结束呢?
很明显,在向下滚动时,低部Item下移,顶部空出来空白区域。所以我们只需要从当前在显示的Item向前遍历,直到index=0即可。
当向上滚动时,顶部Item上移,底部空出来空白区域。所以我们也只需要从当前在显示的顶部Item向上遍历,直到Item结束为止。
1.2 改造CustomLayoutManager
首先,onLayoutChildren
不用改造,只需要改造scrollVerticallyBy
即可。原来的到顶、到底判断和回收越界item的代码都不变:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() <= 0) {
return dy;
}
int travel = dy;
//如果滑动到最顶部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑动到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (travel > 0) {
//需要回收当前屏幕,上越界的View
if (getDecoratedBottom(child) - travel < 0) {
removeAndRecycleView(child, recycler);
continue;
}
} else if (travel < 0) {
//回收当前屏幕,下越界的View
if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
removeAndRecycleView(child, recycler);
continue;
}
}
}
…………
}
在回收越界的holderView之后,我们需要在使用detachAndScrapAttachedViews(recycler);
将现在显示的所有item离屏缓存之前,先得到当前在显示的第一个item和最后一个item的索引,因为如果在将所有item从屏幕上离屏缓存以后,利用getChildAt(int position)
是拿不到任何值的,会返回null,因为现在屏幕上已经没有View存在了。
View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
detachAndScrapAttachedViews(recycler);
mSumDy += travel;
Rect visibleRect = getVisibleArea();
这里需要注意的是,我们在所有的布局操作前,先将移动距离mSumDy进行了累加。因为后面我们在布局item时,会弃用offsetChildrenVertical(-travel)
移动item,而是在布局item时,就直接把item布局在新位置。最后,因为我们已经累加了mSumDy,所以我们需要改造getVisibleArea()
,将原来getVisibleArea(int dy)
中累加dy的操作去掉:
private Rect getVisibleArea() {
Rect result = new Rect(getPaddingLeft(), getPaddingTop() + mSumDy, getWidth() + getPaddingRight(), getVerticalSpace() + mSumDy);
return result;
}
接下来,就是布局屏幕上的所有item,同样是分情况:
if (travel >= 0) {
int minPos = getPosition(firstView);
for (int i = minPos; i < getItemCount(); i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
}
}
}
这里需要注意的是,当dy>0时,表示向上滚动(手指由下向上滑),所以我们需要从之前第一个可见的item向下遍历,因为我们不知道在什么情况下遍历结束,所以我们使用最后一个item的索引(getItemCount()
)做为结束位置。当然大家在这里也可以优化,可以使用下面的语句:
int max = minPos +