从零开始实现SnakeLayoutManager(原创)
从零开始实现SnakeLayoutManager(原创)
界面原型图
没错,就长这个亚子,扭来扭去的。
实际效果图
我们该怎么去思考呢???
其实仔细一点的同学就会发现,这玩意和网格看起来略有相似之处嘛~ 那不妨用RecyclerView的GridLayoutManager去布局咯,事实上这样的方案是可行的,不过需要处理position,在第2行开始(行数从0开始),将position倒一下序才行,而且还需要让第1列(列数就从0开始)和第3列单独占一行,还得判断是在左边还是右边,这样处理起来还是比较麻烦的,因为你需要在适配器、布局管理器等地方进行设置才行,无疑这是一个不太优雅的解决方案。
我们该如何优雅的实现这个布局呢???
我们知道RecyclerView是通过LayoutManager来实现各种各样的布局效果的,也就是说LayoutManger是控制如何摆放ItemView的,所以我们可以自定义LayoutManger,这样我们就可以像 LinearLayoutManager 一样使用这个效果了!那我们就自定义一个SnakeLayoutManager咯~(因为这个布局和一条蛇一样扭来扭去的??)
找布局的规律
在自定义布局管理器之前,我们先来找找这个Snake布局的规律吧,我们先从简单的开始,其他列数的规律其实是类似的,我用Excel画了个表,用来直观的观察这个规律。
可能大家没看出来有啥规律,但是脑瓜子灵光的小伙伴会在这里建立一个坐标系(我就是,哈哈哈哈 皮一下很开心??)
那么position和坐标之间的对应关系如下表格所示
我们可以看出,x轴的坐标是每隔三个相同,然后加1,完了之后再加1,以此类推,y轴的坐标则是按照 0、1、2、2、2、1、0、0 这样的规律重复出现的,既然每行3列的是这样,那效果图中的每行6列是不是也是类似的规律呢?? 答案是肯定的,有兴趣的小伙伴可以自己去试一下,下面给出了每行3列,每行4列,每行5列的效果图进行对照,至于6列的就没画了(还不是因为懒??)。
布局管理器代码部分
既然规律都知道了,那就上代码吧,注释都很详细了,就没必要再解释了,相信大家都能看懂~
public class SnakeLayoutManger extends RecyclerView.LayoutManager {
private static final String TAG = "SnakeLayoutManger";
//默认一行最多几个
public static final int DEFAULT_SPAN_COUNT = 6;
private int mSpanCount = DEFAULT_SPAN_COUNT;
public static final int DEFAULT_TURNING_POINT_POSITION = 5;
//转折点的位置
private int mTurningPointPosition = DEFAULT_TURNING_POINT_POSITION;
public SnakeLayoutManger() {
}
public SnakeLayoutManger(int spanCount, int turningPoint, int... rule) {
mSpanCount = spanCount;
mTurningPointPosition = turningPoint;
mXIndexList.clear();
for (int xPosition : rule) {
mXIndexList.add(xPosition);
}
}
public SnakeLayoutManger(int spanCount, int turningPoint, @NonNull List<Integer> rule) {
mSpanCount = spanCount;
mTurningPointPosition = turningPoint;
mXIndexList.clear();
mXIndexList.addAll(rule);
}
/**
* 获取一行最多有多少列
*
* @return
*/
public int getSpanCount() {
return mSpanCount;
}
/**
* 获取拐点的位置
*
* @return
*/
public int getTurningPointPosition() {
return mTurningPointPosition;
}
/**
* 生成默认布局参数(这个方法必须重写)
*
* @return
*/
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
//是否已经布局过了
private boolean isLayoutChildren = false;
//存放摆放ItemView规律的集合
private List<Integer> mXIndexList = new ArrayList<>();
/**
*
* ==========第一次完整的规律 start=========
* (0,0) (0,1) (0,2)
* (1,2)
* (2,0) (2,1) (2,2)
* (3,0)
* ==========第一次完整的规律 end===========
*
* ==========第二次完整的规律 start=========
* (4,0) (4,1) (4,2)
* (5,2)
* (6,0) (6,1) (6,2)
* (7,0)
* ==========第二次完整的规律 end===========
*
*/
{
//x轴坐标排列的规律(需要一次完整的规律,后续的摆放规律都从这里获取)
mXIndexList.add(0);
mXIndexList.add(1);
mXIndexList.add(2);
mXIndexList.add(3);
mXIndexList.add(4);
mXIndexList.add(5);
mXIndexList.add(5);
mXIndexList.add(5);
mXIndexList.add(4);
mXIndexList.add(3);
mXIndexList.add(2);
mXIndexList.add(1);
mXIndexList.add(0);
mXIndexList.add(0);
}
//1、在RecyclerView初始化时,会被调用两次。
//2、在调用adapter.notifyDataSetChanged()时,会被调用。
//3、在调用setAdapter替换Adapter时,会被调用。
//4、在RecyclerView执行动画时,它也会被调用。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
int itemCount = getItemCount();
//如果Item的个数为0,则认为没必要对ItemView进行布局
//state.isPreLayout() 是否支持动画
if (itemCount == 0 || state.isPreLayout()) return;
if (mXIndexList == null || mXIndexList.size() == 0) {
throw new RuntimeException("======== mXIndexList is null or mXIndexList is empty!");
}
if (isLayoutChildren) return;
//分离并报废附加视图(解除ItemView的绑定)
detachAndScrapAttachedViews(recycler);
//容器的宽度
int containerWidth = getWidth();
//ItemView在y轴上的坐标
int yPosition = 0;
//用来判断是否y轴坐标需要增加
int count = 0;
for (int i = 0; i < itemCount; i++) {
View targetView = measure(recycler, i);
//获得ItemView测量后的宽度
int width = getDecoratedMeasuredWidth(targetView);
//获得ItemView测量后的高度
int height = getDecoratedMeasuredHeight(targetView);
int itemViewSize = Math.min(width, height);
//ItemView的margin值 = (总的宽度 - padding值 - 一行Item所占的总宽度)/ 一行Item的个数
int margin = (containerWidth - getPaddingLeft() - getPaddingRight() - itemViewSize * getSpanCount()) / getSpanCount();
int xIndex = i % mXIndexList.size();
//获取ItemView在x轴上的坐标
Integer xPosition = mXIndexList.get(xIndex);
Log.d(TAG, "currentPosition============>xPosition:" + xPosition + " yPosition:" + yPosition + " index:" + i);
//左边界的位置
int left = xPosition * (width + margin);
//有边界的位置
int right = (xPosition + 1) * (width + margin);
//上边界的位置
int top = yPosition * (height + margin);
//下边界的位置
int bottom = (yPosition + 1) * (height + margin);
//根据边界值摆放ItemView
layoutDecorated(targetView, left, top, right, bottom);
if (betweenAnd(0, getTurningPointPosition(), count)) {
//如果在转折点范围之内,并且等于转折点的x轴坐标,则y轴坐标+1
if (count == getTurningPointPosition()) {
yPosition += 1;
}
count++;
} else {
//如果大于转折点的x轴坐标,将当前次数重新初始化,并将y轴坐标+1
count = 0;
yPosition += 1;
}
}
isLayoutChildren = true;
}
/**
* 判断是否在最大值和最小值之间 [min,max]
*
* @param minValue
* @param maxValue
* @param targetValue
* @return
*/
private boolean betweenAnd(int minValue, int maxValue, int targetValue) {
return targetValue <= maxValue && targetValue >= minValue;
}
/**
* 测量并添加item
*
* @param recycler
* @param index
* @return
*/
private View measure(RecyclerView.Recycler recycler, int index) {
//获取该位置的ItemView
View targetView = recycler.getViewForPosition(index);
//将ItemView添加到容器中
addView(targetView);
//测量带边距的孩子
measureChildWithMargins(targetView, 0, 0);
return targetView;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//界面向下滚动的时候,dy为正,向上滚动的时候dy为负
//填充
//fill(dy, recycler, state);
//滚动
offsetChildrenVertical(-dy);
//回收已经离开界面的
//recycleOut(dy, recycler, state);
return dy;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
//填充
//fill(dy, recycler, state);
offsetChildrenHorizontal(-dx);
//回收已经离开界面的
//recycleOut(dy, recycler, state);
return dx;
}
private void fill(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//向下滚动
if (dy > 0) {
// TODO: 2020/7/5 向底部填充
} else {
//向上滚动
// TODO: 2020/7/5 向顶部填充
}
}
/**
* 回收ItemView
*
* @param dy
* @param recycler
* @param state
*/
private void recycleOut(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
if (view == null) return;
if (dy > 0) {
if (view.getBottom() - dy < 0) {
LogUtils.d("recycleOut==============> " + i);
removeAndRecycleView(view, recycler);
}
} else {
if (view.getTop() - dy > getHeight()) {
LogUtils.d("recycleOut==============> " + i);
removeAndRecycleView(view, recycler);
}
}
}
}
}
写在最后
当然,这个SnakeLayoutManager还有很多可以优化的地方,比如处理滚动事件和复用问题。本文只是介绍如何进行布局,感兴趣的同学可以自己实现一下。
如果觉得本文不错,点个赞再走呗~
允许转载至《阳光沙滩》微信公众号,其他转载请联系:2695734816@qq.com