1.纵向Layoutmanager(VerticalLayoutManager)
- 先写一个类继承Layoutmanager,默认要实现generateDefaultLayoutParams方法,一般没有要修改itemview布局参数的话,默认就按下面来写
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
2.如果只重写这个方法的话,我们运行起来会发现什么都没有显示,这是因为所有的itemView的布局都是在onLayoutChildren方法中,所以我们要重写这个方法来布局所有的itemview
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return;
}
//摆放可见的itemview
int temp = 0;
for (int i = 0; i < getItemCount(); i++) {
View child = recycler.getViewForPosition(i);
measureChildWithMargins(child, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(child);
int itemHeight = getDecoratedMeasuredHeight(child);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, getPaddingLeft(), temp, itemWidth, temp + itemHeight);
temp += itemHeight;
}
mTotalHeight = Math.max(getVerticalSpace(), temp);
}
- getItemCount()==0的话代表数据为0我们直接return
- 遍历RecycleView所有的条目
- 每个条目的宽高都一样,所以每个条目的left和right一样,只需要将每个条目的高度累加就可以将所有条目纵向排列起来
-
必须先测量再拿宽高否则拿不到
3.你会发现此时不可以上下滑动,想要上下滑动还需要重写下面二个方法
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
offsetChildrenVertical(-dy);
return dy;
}
- canScrollVertically方法返回ture代表可以竖直滑动
- scrollVerticallyBy方法中 dy>0代表上滑,dy<0代表下滑
- 用offsetChildrenVertical方法来移动所有的item
4.你会发现itemview可以拖出屏幕之外,所以要对拖动范围进行限制,上滑不能超出RecycleView高度,下滑不能滑动到0以下
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return dy;
}
int travel = dy;
//限定拖动范围
if (mTotalMoveY + travel < 0) {
travel = -mTotalMoveY;
} else if (mTotalMoveY + travel > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
}
mTotalMoveY += travel;
offsetChildrenVertical(-travel);
return travel;
}
2.纵向复用(VerticalLayoutManagerRecycled)
1.上面效果看上去和LinerLayoutManager没有什么区别,但是我们在Adapter的onCreateViewHolder和onBindViewHolder方法中打印Log会发现有多少条目就同时调用了多少次onCreateViewHolder和onBindViewHolder方法,onCreateViewHolder执行一次代表创建了一个itemview,这就代表没有复用itemview,在数据量大的情况下就会发生anr异常
2.复用用到的几个重要的方法:
public void detachAndScrapAttachedViews(Recycler recycler)
仅用于onLayoutChildren中,在布局前,将所有在显示的HolderView从RecyclerView中剥离,将其放在mAttachedScrap中,以供重新布局时使用View view = recycler.getViewForPosition(position)
用于向RecyclerView申请一个HolderView,这个HolderView是从缓存池子拿的,正是这个函数能为我们实现复用。removeAndRecycleView(child, recycler)
这个函数仅用于滚动的时候,在滚动时,我们需要把滚出屏幕的HolderView标记为Removed,这个函数的作用就是把已经不需要的HolderView标记为Removed。在我们标记为Removed以为,会把这个HolderView移到mCachedViews中,如果mCachedViews已满,就利用先进先出原则,将mCachedViews中老的holderView移到mRecyclerPool中,然后再把新的HolderView加入到mCachedViews中。int getPosition(View view)
这个函数用于得到某个View在Adapter中的索引位置,我们经常将它与getChildAt(int position)联合使用,得到某个当前屏幕上在显示的View在Adapter中的位置
3.具体代码
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return;
}
detachAndScrapAttachedViews(recycler);
View view = recycler.getViewForPosition(0);
measureChildWithMargins(view, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(view);
int itemHeight = getDecoratedMeasuredHeight(view);
//屏幕上可见的itemView个数
int visible = getVerticalSpace() / itemHeight;
int temp = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(getPaddingLeft(), temp, itemWidth, itemHeight + temp);
mRectArray.put(i, rect);
temp += itemHeight;
}
//摆放可见的itemview
for (int i = 0; i < visible; i++) {
Rect rect = mRectArray.get(i);
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top, rect.right, rect.bottom);
}
mTotalHeight = Math.max(getVerticalSpace(), temp);
}
- 先调用detachAndScrapAttachedViews方法把所有可见的itemview剥离
- 定义一个集合存储Rect,每个Rect都记录了每个itemView的位置
- 一屏中能放几个item就获取几个HolderView,撑满初始化的一屏即可,不要多创建,visible 代表可见的itemView个数
//dy>0 dy<0
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return dy;
}
int travel = dy;
if (travel + mTotalMoveY < 0) {
travel = -mTotalMoveY;
} else if (travel + mTotalMoveY > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
}
//回收不在屏幕内的itemview
for (int i = getChildCount()-1; i >=0; i--) {
View view = getChildAt(i);
//下滑回收上面移出屏幕的itemview
if (getDecoratedBottom(view) - travel < 0) {
removeAndRecycleView(view, recycler);
//上滑回收下面移出屏幕的itemview
} else if (getDecoratedTop(view) - travel > getVerticalSpace()) {
removeAndRecycleView(view, recycler);
}
}
//获取当前可见的屏幕区域
Rect visibleRect = getVisibleRect(travel);
//移动时将回收的itemview从缓存中取出来
if (travel > 0) {
//上滑即将出来的条目的索引
int next = getPosition(getChildAt(getChildCount() - 1)) + 1;
for (int i = next; i < getItemCount(); i++) {
insertView(recycler, visibleRect, i, false);
}
} else {
//下滑即将出来的条目的索引
int last = getPosition(getChildAt(0)) - 1;
for (int i = last; i >= 0; i--) {
insertView(recycler, visibleRect, i, true);
}
}
mTotalMoveY += travel;
offsetChildrenVertical(-travel);
return travel;
}
private void insertView(RecyclerView.Recycler recycler, Rect visibleRect, int pos, boolean flag) {
Rect rect = mRectArray.get(pos);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(pos);
if (flag) {
addView(child, 0);
} else {
addView(child);
}
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mTotalMoveY,
rect.right, rect.bottom - mTotalMoveY);
}
}
private int getVerticalSpace() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
private Rect getVisibleRect(int travel) {
return new Rect(getPaddingLeft(), mTotalMoveY + travel,
getWidth(), mTotalMoveY + travel + getVerticalSpace());
}
- 回收不在屏幕内的itemview中遍历屏幕内的itemview,getDecoratedBottom(view) - travel < 0代表上滑屏幕内的第一个itemview即将<0超出屏幕所以remove掉
- getDecoratedTop(view) - travel > getVerticalSpace()代表下滑屏幕内的最后一个itemview即将超出屏幕所以remove掉
- int next = getPosition(getChildAt(getChildCount() - 1)) + 1代表上滑时即将出现的条目,从这开始遍历把即将出现的条目都添加进来
-
int last = getPosition(getChildAt(0)) - 1代表下滑时即将出现的条目
- 此时打Log可以看到在调用了几次onCreateViewHolder以后都是调用onBindViewHolder代表实现了复用
12-19 11:57:51.996 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+49
12-19 11:57:51.997 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+50
12-19 11:57:51.998 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+51
12-19 11:57:52.012 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+52
12-19 11:57:52.013 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+53
12-19 11:57:52.013 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+54
12-19 11:57:52.029 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+55
12-19 11:57:52.030 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+56
12-19 11:57:52.030 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+57
12-19 11:57:52.045 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+58
12-19 11:57:52.046 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+59
12-19 11:57:52.048 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+60
12-19 11:57:52.061 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+61
12-19 11:57:52.062 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+62
12-19 11:57:52.062 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+63
12-19 11:57:52.078 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+64
12-19 11:57:52.079 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+65
3.纵向Layoutmanager带动画(VerticalLayoutManagerAnim)
1.使用offsetChildrenVertical(-travel)函数来移动屏幕中所有item。这种方法仅适用于每个item,在移动时,没有特殊效果的情况,当我们在移动item时,同时需要改变item的角度、透明度等情况时,单纯使用offsetChildrenVertical(-travel)来移是不行的。针对这种情况,我们就只有使用第二种方法来实现回收复用了。
2.我们主要替换掉移动item所用的offsetChildrenVertical(-travel);函数,既然要将它弃用,那我们就只能自己布局每个item了。
3.具体代码
- 定义一个集合来存储已经布局的itemview的position
//是否在当前屏幕的itemView
private SparseBooleanArray mAttachItems = new SparseBooleanArray();
- 在onLayoutChildren中进行默认全部为false即没有进行过布局
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(getPaddingLeft(), temp, itemWidth, itemHeight + temp);
mRectArray.put(i, rect);
mAttachItems.put(i, false);
temp += itemHeight;
}
//dy>0 dy<0
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return dy;
}
int travel = dy;
if (travel + mTotalMoveY < 0) {
travel = -mTotalMoveY;
} else if (travel + mTotalMoveY > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
}
mTotalMoveY += travel;
//获取当前可见的屏幕区域
Rect visibleRect = getVisibleRect();
//回收不在屏幕内的itemview
for (int i = getChildCount()-1; i >= 0; i--) {
View view = getChildAt(i);
int position = getPosition(view);
Rect rect = mRectArray.get(position);
if (Rect.intersects(visibleRect, rect)) {
layoutDecoratedWithMargins(view, rect.left, rect.top - mTotalMoveY, rect.right, rect.bottom - mTotalMoveY);
mAttachItems.put(position, true);
view.setRotationY(view.getRotationY() + 1);
} else {
removeAndRecycleView(view, recycler);
mAttachItems.put(position, false);
}
}
int next = getPosition(getChildAt(getChildCount() - 1));
int last = getPosition(getChildAt(0));
//移动时将回收的itemview从缓存中取出来
if (travel > 0) {
//上滑即将出来的条目的索引
for (int i = next; i < getItemCount(); i++) {
insertView(recycler, visibleRect, i, false);
}
} else {
//下滑即将出来的条目的索引
for (int i = last; i >= 0; i--) {
insertView(recycler, visibleRect, i, true);
}
}
return travel;
}
private void insertView(RecyclerView.Recycler recycler, Rect visibleRect, int pos, boolean flag) {
Rect rect = mRectArray.get(pos);
if (Rect.intersects(visibleRect, rect) && !mAttachItems.get(pos)) {
View child = recycler.getViewForPosition(pos);
if (flag) {
addView(child, 0);
} else {
addView(child);
}
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mTotalMoveY,
rect.right, rect.bottom - mTotalMoveY);
child.setRotationY(child.getRotationY() + 1);
mAttachItems.put(pos, true);
}
}
- 我们先进行对当前屏幕的itemview进行遍历如果在屏幕内直接布局否则就remove掉然后把它们的状态都存起来,在屏幕内的我们给它设置了一个 view.setRotationY(view.getRotationY() + 1)动画
-
下面同样进行即将出现的条目进行添加 if (Rect.intersects(visibleRect, rect) && !mAttachItems.get(pos)) ,如果在屏幕内并且没有进行过布局的就add进来,然后进行状态存储和设置动画
4.横向滑动HorizontalLayoutmanager与流失布局效果FlowLayoutmanager
-
复用未带动画
-复用带动画
- 横向的LayoutManager与纵向的大体一致,只不过布局摆放有所不同,这里不在赘述
- FlowLayoutManager复用和滑动都和VerticalLayoutManager的scrollVerticallyBy方法一致只是onLayoutChildren有所不同
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return;
}
detachAndScrapAttachedViews(recycler);
int tempHeight = mDefaultMargin;
int tempWidth = mDefaultMargin;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(view);
int itemHeight = getDecoratedMeasuredHeight(view);
//超出屏幕宽度换行
if (tempWidth + itemWidth >= getHorizontalSpace()) {
tempWidth = mDefaultMargin;
tempHeight += itemHeight + mDefaultMargin;
}
layoutDecoratedWithMargins(view, tempWidth, tempHeight, itemWidth + tempWidth, tempHeight + itemHeight);
tempWidth += itemWidth + mDefaultMargin;
}
mTotalHeight = Math.max(tempHeight, getVerticalSpace());
}
- 遍历所有的item如果宽度加起来超出屏幕宽度换行,height累加,width重置这样就可以实现效果
源码地址https://github.com/digtal/recycleview-study
本篇内容参考于https://blog.csdn.net/harvic880925/article/details/84979161