Android TV Recyclerview长按或连续按键,焦点丢失(或者焦点跳跃)
原因分析
RecyclerView设置适配器后,将数据填充进去,并不会将所有item的view都创建出来,一般只会创建一个屏幕的Item,当长按或者快速按下键时,Recyclerview来不及创建即将获取焦点的view,导致焦点丢失
解决方法
有两种思路:
(1)控制按键速度
这里有两种具体实现策略:
一种是记录上次按下时间,然后在下次按下时间时去计算与上次按下时间的间隔,如果间隔小于某个值(比如1秒),我们就不处理该次按下事件(具体是在dispatchKeyEvent、或者onKeyDown、或者onKeyUp中return true),
这里需要注意的是,只有Activity有这些处理方法,Fragment的按键拦截也需要在依附的Activity中进行处理
另一种是是在dispatchKeyEvent、或者onKeyDown、或者onKeyUp中,拿到KeyEvent。通过KeyEvent.getRepeatCount()计算是否是长按,当getRepeatCount()大于0则是长按,值越大表示用户长按事件越长。
然后我们可以拦截长按事件(同样return true)
(2)对Recyclerview设置LayoutManager,在LayoutManager中控制焦点
在RecyclerView的LayoutManager中,有这样一个方法onInterceptFocusSearch(View focused, int direction),这个方法就是用于寻找焦点的。当遇到长按或者连续按键焦点飞掉的情况时,需要重载RecyclerView的LayoutManager,重写此方法。
在实践中有两种具体情况:网格布局和线性布局,其实就是对RecyclerView设置LinearLayoutManager或GridLayoutManager,处理上大同小异。
(3)对RecyclerView添加滚动监听
RecyclerView.addOnScrollListener(gridScrollListener),原理就是监听可见条目在RecyclerView已加载数据中位置,在合适时候提前去加载下一页数据,避免等滑动到最后再去加载或请求下一页,请求数据时间过长.
这里是我项目中的实现思路和代码,给我们的列表添加滚动监听如下
val gridScrollListener: RecyclerView.OnScrollListener = object : RecyclerView.OnScrollListener() { @RequiresApi(Build.VERSION_CODES.N) override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val lastVisible = gridLayoutManager.findLastVisibleItemPosition() val itemCount = gridLayoutManager.itemCount if (lastVisible + gridLayoutManager.spanCount >= itemCount - 1) { //当最后的一条可见item位置比目前所有item的条数+spancount之和还大时,此时提前加载下一页 loadMoreConferenceList()//加载下一页数据 } } }
自定义LinearLayoutManager
public class ScrollControlLayoutManager extends LinearLayoutManager { private static final String TAG = "ScrollControlLayoutMana"; public ScrollControlLayoutManager(Context context) { super(context); } public ScrollControlLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } public ScrollControlLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { super.onLayoutChildren(recycler, state); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } } @Override public View onInterceptFocusSearch(View focused, int direction) { int count = getItemCount();//获取item的总数 int fromPos = getPosition(getFocusedChild());//当前焦点的位置 int lastVisibleItemPos = findLastVisibleItemPosition();//最新的已显示的Item的位置 switch (direction) {//根据按键逻辑控制position case View.FOCUS_RIGHT: case View.FOCUS_DOWN: fromPos++; break; case View.FOCUS_LEFT: case View.FOCUS_UP: fromPos--; break; } Log.i(TAG, "onInterceptFocusSearch , fromPos = " + fromPos + " , count = " + count+" , lastVisibleItemPos = "+lastVisibleItemPos); // if(fromPos < 0 || fromPos > count ) { // //如果下一个位置<0,或者超出item的总数,则返回当前的View,即焦点不动 // return focused; // } else { // //如果下一个位置大于最新的已显示的item,即下一个位置的View没有显示,则滑动到那个位置,让他显示,就可以获取焦点了 // if (fromPos >= 0 && fromPos < count && fromPos > lastVisibleItemPos) { // scrollToPosition(fromPos); // } // } if (fromPos >= 0 && fromPos < count && fromPos > lastVisibleItemPos) { scrollToPosition(fromPos); } return super.onInterceptFocusSearch(focused, direction); } }
自定义GridLayoutManagerpublic
class ScrollControlGridLayoutManager extends GridLayoutManager{ private static final String TAG = "ScrollControlLayoutMana"; public ScrollControlGridLayoutManager(Context context, int span) { super(context, span); } public ScrollControlGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { super.onLayoutChildren(recycler, state); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } } @Override public View onInterceptFocusSearch(View focused, int direction) { int count = getItemCount();//获取item的总数 int fromPos = getPosition(getFocusedChild());//当前焦点的位置 int lastVisibleItemPos = findLastVisibleItemPosition();//最新的已显示的Item的位置 switch (direction) {//根据按键逻辑控制position case View.FOCUS_RIGHT: case View.FOCUS_DOWN: fromPos+=getSpanCount(); break; case View.FOCUS_LEFT: case View.FOCUS_UP: fromPos-=getSpanCount(); break; } Log.i(TAG, "onInterceptFocusSearch , fromPos = " + fromPos + " , count = " + count+" , lastVisibleItemPos = "+lastVisibleItemPos); // if(fromPos < 0 || fromPos > count ) { // //如果下一个位置<0,或者超出item的总数,则返回当前的View,即焦点不动 // return focused; // } else { // //如果下一个位置大于最新的已显示的item,即下一个位置的View没有显示,则滑动到那个位置,让他显示,就可以获取焦点了 // if (fromPos < count && fromPos > lastVisibleItemPos) { // scrollToPosition(fromPos); // } // } if (fromPos >= 0 && fromPos > lastVisibleItemPos) { scrollToPosition(fromPos); } return super.onInterceptFocusSearch(focused, direction); }}
总结:
在下一次滚动前,计算即将显示的item位置,如果下一个位置大于最新的显示的item(即下一个位置的view没有显示),
则滑动到那个位置,并使其显示获取焦点
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 【.NET】调用本地 Deepseek 模型