RecyclerView自定义StaggeredGridLayoutManager实现EPG布局

本文主要是介绍RecyclerView自定义StaggeredGridLayoutManager实现EPG布局,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

实现布局:

最近在做EPG功能,要求能够展示每个节目对应时间轴的预览节目信息,这种布局可以采用RecyclerView的瀑布流布局实现,但是测试的时候发现Item的position是没有规律的,这样就不能够很好的根据positoin去区分是哪个节目的EPG信息从而显示到不同行数上,所以需要从StaggeredGridLayoutManager源码改起

修改getNextSpan函数

/*** Finds the span for the next view.*/
private StaggeredGridLayoutManager.Span getNextSpan(LayoutState layoutState) {final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection);final int startIndex, endIndex, diff;if (preferLastSpan) {startIndex = mSpanCount - 1;endIndex = -1;diff = -1;} else {startIndex = 0;endIndex = mSpanCount;diff = 1;}//布局排列方向,表示从起始排到末尾if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {Span min = null;int minLine = Integer.MAX_VALUE;final int defaultLine = mPrimaryOrientation.getStartAfterPadding();//for循环是找到布局中哪个列的最后一个Item的末尾处坐标最小for (int i = startIndex; i != endIndex; i += diff) {final Span other = mSpans[i];int otherLine = other.getEndLine(defaultLine);if (otherLine < minLine) {min = other;minLine = otherLine;}}return min;} else { //布局排列方向,表示从末尾排到起始Span max = null;int maxLine = Integer.MIN_VALUE;final int defaultLine = mPrimaryOrientation.getEndAfterPadding();//for循环是找到布局中哪个列的第一个Item的起始处坐标值最大for (int i = startIndex; i != endIndex; i += diff) {final Span other = mSpans[i];int otherLine = other.getStartLine(defaultLine);if (otherLine > maxLine) {max = other;maxLine = otherLine;}}return max;}
}

该函数是获取下一个Span,每次获取都会先计算当前哪个Span的末尾处最小,最小则返回该Span,以便下一个View添加到这个Span上,我们可以增加一个int变量,每次调用+1,循环去Span数组获取,修改layoutState.mLayoutDirection == LayoutState.LAYOUT_END条件代码为

if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {return mSpans[(index++) % mSpanCount];
}

这样我们就可以在adapter里面根据position去区分是哪一行了

虽然上面代码能够有规律的显示数据了,但是当我们回滚时,会发现行数据会概率发现交换,通过打印代码发现,行数据发生交换的时候会调用getNextSpan函数我们修改的代码块,index++了,所以行数据会发生交换,既然知道了原因,那么我们来看看为什么会调用到这个函数吧

在fill(EPGRecyclerView.Recycler recycler, LayoutState layoutState,EPGRecyclerView.State state)这个函数里,有这样一段代码

final int spanIndex = mLazySpanLookup.getSpan(position);
StaggeredGridLayoutManager.Span currentSpan;
final boolean assignSpan = spanIndex == StaggeredGridLayoutManager.LayoutParams.INVALID_SPAN_ID;
if (assignSpan) {currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState);mLazySpanLookup.setSpan(position, currentSpan);if (DEBUG) {Log.d(TAG, "assigned " + currentSpan.mIndex + " for " + position);}
}

当我们回滚时,按道理assignSpan应该是false的,spanIndex应该是有确切值的,而且spanIndex设置只有一处,这说明滚动时候在某个地方spanIndex被重置了

通过对代码的debug调试,原来是onLayoutChildren函数中有一个判断2个相邻的子View位置是否合理的逻辑,如果不合理的话会进行变量的重置,也就是上面的spanIndex

onLayoutChildren函数中有这样一段代码

boolean hasGaps = false;
if (shouldCheckForGaps && !state.isPreLayout() ) {//LogUtil.e("StaggeredGridLayoutManager", "onLayoutChildren", "state.isPreLayout() = false");final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE&& getChildCount() > 0&& (mLaidOutInvalidFullSpan || hasGapsToFix() != null);if (needToCheckForGaps) {removeCallbacks(mCheckForGapsRunnable);//LogUtil.e("StaggeredGridLayoutManager", "onLayoutChildren", "checkForGaps()3");if (checkForGaps()) {hasGaps = true;}}
}
if (state.isPreLayout()) {mAnchorInfo.reset();
}
mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd;
mLastLayoutRTL = isLayoutRTL();
if (hasGaps) {//进行重置mAnchorInfo.reset();onLayoutChildren(recycler, state, false);
}

接下来我们来看看hasGapsToFix里面的一段代码

if (mShouldReverseLayout) {// ensure child's end is below nextChild's endint myEnd = mPrimaryOrientation.getDecoratedEnd(child);int nextEnd = mPrimaryOrientation.getDecoratedEnd(nextChild);if (myEnd < nextEnd) {return child; //i should have a better position} else if (myEnd == nextEnd) {compareSpans = true;}
} else {int myStart = mPrimaryOrientation.getDecoratedStart(child);int nextStart = mPrimaryOrientation.getDecoratedStart(nextChild);//如果当前View的起始处大于下一个View的起始处,会返回当前Viewif (myStart > nextStart) {return child; //i should have a better position} else if (myStart == nextStart) {compareSpans = true;}
}
if (compareSpans) {// equal, check span indices.StaggeredGridLayoutManager.LayoutParams nextLp = (StaggeredGridLayoutManager.LayoutParams) nextChild.getLayoutParams();if (lp.mSpan.mIndex - nextLp.mSpan.mIndex < 0 != preferredSpanDir < 0) {//preferredSpanDir < 0 为truereturn child;}
}

既然知道了原因,那么我们可以修改为不检测间隙,这样就避免了行数据交换的问题

①增加一个needCheckGap的标志位,置为false

//增加一个是否需要检测间隙的标志
private boolean needCheckGap = false;

②在hasGapsToFix函数起始处直接返回

View hasGapsToFix() {//不需要检测间隙 -zbjif (!needCheckGap) {return null;}...
}

③onLayoutChild函数去除检测间隙

boolean hasGaps = false;
//不检测间隙
if (shouldCheckForGaps && !state.isPreLayout() && hasGaps) {//LogUtil.e("StaggeredGridLayoutManager", "onLayoutChildren", "state.isPreLayout() = false");final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE&& getChildCount() > 0&& (mLaidOutInvalidFullSpan || hasGapsToFix() != null);if (needToCheckForGaps) {removeCallbacks(mCheckForGapsRunnable);//LogUtil.e("StaggeredGridLayoutManager", "onLayoutChildren", "checkForGaps()3");if (checkForGaps()) {hasGaps = true;}}
}

到这里,EPG功能布局已经差不多了,可以正常在手机上显示了,但是在安卓TV上,用遥控器左右键来进行选择时,会出现焦点不在同一行的问题,经过对安卓焦点规律的研究,发现是当滚动到屏幕后面的时候,后面已经没有实际的View了,这时候安卓系统会就近选择View,也就会出现左右切换焦点不在同一行的问题,解决这个问题也挺简单的,就是让RecyclerView额外多加载一层View,onLayoutChildren函数主要是进行子View的添加的,所以我们要从这里开始研究,该函数中有这样的代码块

// Layout end.
setLayoutStateDirection(LayoutState.LAYOUT_END);
testNum = 3;
fill(recycler, mLayoutState, state);
// Layout start.
setLayoutStateDirection(LayoutState.LAYOUT_START);
mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state);

这里有2个fill函数,其实都是填充布局的,只不过第1个是为了做一些边缘检测处理,第二个才是真正的进行填充布局,接下来看看fill函数,StaggeredGridLayoutManager有一个判断某列或行是否还需要进行填充布局的标志mRemainingSpans,fiil函数填充布局的时候就会去对这个标志位做判断和赋值,以便用来加载子View

fill函数起始处会对mRemainingSpans做一个复位,也就是所有的列或行都需要进行添加子View

mRemainingSpans.set(0, mSpanCount, true);

接着在while循环中有这样一段代码,主要是用于检测该列或行末尾是否已经达到需要展示的位置,如果达到的话就将该列的标志位置为false,也就不再需要进行添加子View了

if (lp.mFullSpan) {updateAllRemainingSpans(mLayoutState.mLayoutDirection, targetLine);
} else {updateSpanNum = 1;updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
}
private void updateRemainingSpans(StaggeredGridLayoutManager.Span span, int layoutDir, int targetLine) {final int deletedSize = span.getDeletedSize();final int line;if (layoutDir == LayoutState.LAYOUT_START) {line = span.getStartLine();if (line + deletedSize <= targetLine) {mRemainingSpans.set(span.mIndex, false);}} else {line = span.getEndLine();if (line - deletedSize >= targetLine) {mRemainingSpans.set(span.mIndex, false);}}
}

知道了填充布局的原理,那么我们就添加多一个BitSet标志位来使mRemainingSpans延迟置位,以达到多填充一层子View的目的

定义一个全局BitSet

/*** 增加一个延迟置位的标志,使其每列/行能添加多一个View */
private BitSet mDelayFlagSpans;

在setSpanCount(int spanCount)函数进行初始化

public void setSpanCount(int spanCount) {...mRemainingSpans = new BitSet(mSpanCount);mDelayFlagSpans = new BitSet(mSpanCount);... 
}

接着在fill函数起始处添加置位标志

mRemainingSpans.set(0, mSpanCount, true);
mDelayFlagSpans.set(0, mSpanCount, true);

在updateRemainingSpans函数中对mRemainingSpans做延迟操作

private void updateRemainingSpans(StaggeredGridLayoutManager.Span span, int layoutDir, int targetLine) {final int deletedSize = span.getDeletedSize();final int line;if (layoutDir == LayoutState.LAYOUT_START) {line = span.getStartLine();if (line + deletedSize <= targetLine) {if (mDelayFlagSpans.get(span.mIndex)) {mDelayFlagSpans.set(span.mIndex, false);} else {mRemainingSpans.set(span.mIndex, false);}}} else {line = span.getEndLine();if (line - deletedSize >= targetLine) {if (mDelayFlagSpans.get(span.mIndex)) {mDelayFlagSpans.set(span.mIndex, false);} else {mRemainingSpans.set(span.mIndex, false);}}}
}

到这里差不多了,我们可以运行了,但是通过打印发现当一滚动的时候position会一下子多加载了几十个,这样的结果我们是不能接受的,既然一滚动就会多加载,那么肯定是在滚动的时候会调用到fill函数,果不其然,在scrollBy函数中调用到了fill函数

int scrollBy(int dt, EPGRecyclerView.Recycler recycler, EPGRecyclerView.State state) {...int consumed = fill(recycler, mLayoutState, state);...
}

由于scrollBy函数会调用很多次,所以会额外加载了更多的子View,所以我们不能在fill函数里面对mDelayFlagSpans做复位操作,通过对代码的研究,我发现在3处地方加入mDelayFlagSpans的复位操作比较好,第1,2处是在onLayoutChild函数

// Layout end.
setLayoutStateDirection(LayoutState.LAYOUT_END);
mDelayFlagSpans.set(0, mSpanCount, true);
fill(recycler, mLayoutState, state);
// Layout start.
setLayoutStateDirection(LayoutState.LAYOUT_START);
mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection;
testNum = 4;
mDelayFlagSpans.set(0, mSpanCount, true);
fill(recycler, mLayoutState, state);

一开始布局的时候我们可以对mDelayFlagSpans进行复位,后面滚动的时候是不会在调用到此段代码,第3处是在RecyclerView的dispatchKeyEvent函数对按键做一下判断,并在StaggeredGridLayoutManager中加个对mDelayFlagSpans的复位函数以便在RecyclerView中调用

@Override
public boolean dispatchKeyEvent(KeyEvent event) {int keyCode = event.getKeyCode();// 这里只考虑水平移动的情况(垂直移动相同的解决方案)if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {if (event.getAction() == KeyEvent.ACTION_DOWN) {if(getLayoutManager() instanceof StaggeredGridLayoutManager){StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)getLayoutManager();staggeredGridLayoutManager.resetDelayFlagSpans();}}}return super.dispatchKeyEvent(event);
}

StaggeredGridLayoutManager增加函数

public void resetDelayFlagSpans(){mDelayFlagSpans.set(0, mSpanCount, true);
}

到这里,我们已经基本完成了对StaggeredGridLayoutManager的修改,这里的修改只适合屏幕不做横竖屏切换的情况,因为去除了判断间隙,所以横竖屏切换会出现间隙

这篇关于RecyclerView自定义StaggeredGridLayoutManager实现EPG布局的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/213978

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、