From here https://blog.csdn.net/zxt0601/article/details/52956504
照着写一遍,熟悉代码逻辑,修正那块看着脑袋晕,暂不考虑,复制作者代码就行。
下边是效果图,增加了一个boolean,默认为false,如果为true的话就是下图效果,每行控件都居中显示。
代码如下:
package com.charliesong.demo0327;
import android.graphics.Rect;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by charlie.song on 2018/5/8.
*/
public class FlowGravityLayoutManager extends RecyclerView.LayoutManager {
private int mVerticalOffset;//竖直偏移量 每次换行时,要根据这个offset判断
private int mFirstVisiPos;//屏幕可见的第一个View的Position
private int mLastVisiPos;//屏幕可见的最后一个View的Position
private OrientationHelper orientationHelper;//系统自带的类,这里选择的是垂直方向的,用来获取top,bottom,以及child的宽高等
private SparseArray mItemRects;//key 是position,保存的是view的bound属性
private boolean horizontalCenter=false;//同一行的控件是否居中显示
public FlowGravityLayoutManager() {
orientationHelper = OrientationHelper.createOrientationHelper(this, OrientationHelper.VERTICAL);
mItemRects = new SparseArray<>();
setAutoMeasureEnabled(true);
}
public void setHorizontalCenter(boolean horizontalCenter) {
this.horizontalCenter = horizontalCenter;
}
@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) {
detachAndScrapAttachedViews(recycler);
oneRowViews.clear();
return;
}
if (getChildCount() == 0 && state.isPreLayout()) {
return;
}
detachAndScrapAttachedViews(recycler);
mVerticalOffset = 0;
mFirstVisiPos = 0;
mLastVisiPos = getItemCount() ;
fill(recycler, state);
}
private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
fill(recycler, state, 0);
}
int leftOffset = getPaddingLeft();
int lineMaxHeight = 0;//每行的最大高度,因为同一行的控件可能高度不一样,取最大值,好决定下一行的位置
int topOffset;
SparseArray oneRowViews=new SparseArray<>();//保存在同一行的view,方便在这一行控件加载完以后平移,只对居中对齐的情况下处理
/**
* 填充childView的核心方法,应该先填充,再移动。
* 在填充时,预先计算dy的在内,如果View越界,回收掉。
* 一般情况是返回dy,如果出现View数量不足,则返回修正后的dy.
*
* @param dy RecyclerView给我们的位移量,+,显示底端, -,显示头部,手指往上滑是正的,往下滑是负的
* @return 修正以后真正的dy(可能剩余空间不够移动那么多了 所以return <|dy|)
*/
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {
topOffset = orientationHelper.getStartAfterPadding();
//回收越界子View
if (getChildCount() > 0) {//滑动时进来的
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (dy > 0) {//上滑,顶部的view可能消失
if (getDecoratedBottom(child) - dy < topOffset) {
removeAndRecycleView(child, recycler);
mFirstVisiPos++;
}
} else if (dy < 0) {//手指往下滑,底部的view 可能消失
if (getDecoratedTop(child) - dy > orientationHelper.getEndAfterPadding()) {
removeAndRecycleView(child, recycler);
mLastVisiPos--;
}
}
}
}
//
leftOffset = getPaddingLeft();
lineMaxHeight = 0;
//布局子View阶段
if (dy >= 0) {
int minPos = mFirstVisiPos;
mLastVisiPos = getItemCount() - 1;
if (getChildCount() > 0) {
View lastVisibleView = getChildAt(getChildCount() - 1);
minPos = getPosition(lastVisibleView) + 1;
topOffset = getDecoratedTop(lastVisibleView);
leftOffset = getDecoratedRight(lastVisibleView);
lineMaxHeight = Math.max(lineMaxHeight, orientationHelper.getDecoratedMeasurement(lastVisibleView));
}
for (int i = minPos; i <=mLastVisiPos; i++) {
//找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
if (leftOffset + orientationHelper.getDecoratedMeasurementInOther(child) <= getWidth() - getPaddingRight()) {//这一行可以装的下,不用换行
layoutChild(child,i);
} else {
transViewX();
leftOffset = getPaddingLeft();
topOffset += lineMaxHeight;
lineMaxHeight = 0;
//新起一行的时候要判断一下边界
if (topOffset - dy > orientationHelper.getEndAfterPadding()) {
removeAndRecycleView(child, recycler);
mLastVisiPos=i-1;//已经跑到recyclerview最底部,不可见了,修改mLastVisPos,循环结束
} else {
layoutChild(child,i);
}
}
}
//添加完后,判断是否已经没有更多的ItemView,并且此时屏幕仍有空白,则需要修正dy
View lastVisibleView = getChildAt(getChildCount() - 1);
int lastPosition = getPosition(lastVisibleView);
if (lastPosition == getItemCount() - 1) {
int gap = orientationHelper.getEndAfterPadding() - getDecoratedBottom(lastVisibleView);
if (gap > 0) {
dy -= gap;
}
}
} else {
int maxPosition = getItemCount() - 1;
mFirstVisiPos = 0;
if (getChildCount() > 0) {
View firstView = getChildAt(0);
maxPosition = getPosition(firstView) - 1;
}
for (int i = maxPosition; i >= mFirstVisiPos; i--) {
Rect rect = mItemRects.get(i);
if (rect.bottom - mVerticalOffset - dy < getPaddingTop()) {
mFirstVisiPos = i + 1;
break;
} else {
View child = recycler.getViewForPosition(i);
addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mVerticalOffset, rect.right, rect.bottom - mVerticalOffset);
}
}
}
return dy;
}
//换行或者add最后一个view的时候,平移下最终的位置,
private void transViewX(){
if(!horizontalCenter){
return;
}
int transX=(getWidth()-getPaddingRight()-leftOffset)/2;
for(int i=0;i 0){
//利用最后一个子View比较修正
View lastView=getChildAt(getChildCount()-1);
if(getPosition(lastView)==getItemCount()-1){
int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastView);
if (gap > 0) {
realOffset = -gap;
} else if (gap == 0) {
realOffset = 0;
} else {
realOffset = Math.min(realOffset, -gap);
}
}
}
realOffset= fill(recycler, state, realOffset);//先填充,再位移。
mVerticalOffset += realOffset;//累加实际滑动距离
offsetChildrenVertical(-realOffset);//滑动
return realOffset;
}
}
简单记录下修改代码碰到的问题:
居中显示
最开始我的思路是用个tag【默认是0】,然后每次换行,平移完控件以后,tag就修改为换行后的position
结果我发现没有效果,offsetLeftAndRight不行我换成layoutDecoratedWithMargins也不行,这就奇怪了。
后来想着可能这个child不是我们要的child,也就是说通过recycler.getViewForPosition(start),虽然里边的pisition是一样的,可获取到的child不是一个东西,所以操作没反应。
最后就是上边的代码了,将每行的child,都放到oneRowViews里,换行的时候取出来进行平移,果然没问题。
//这里的代码最早就是写在换行的逻辑里的,结果无效
int transX=(getWidth()-getPaddingRight()-leftOffset)/2;
for(int start=tag;start