Android TV项目要求为RecyclerView列表增加焦点循环与翻页功能,本身功能不难实现,就是滚动列表,然后重定位焦点即可;
不过实际测试发现经常有焦点丢失的问题,让人十分头痛。
先看其中一个现象:
现象:焦点位于第一个item,向上移动,焦点定位到最后一个item,向下移动,焦点重新回到第一个item,再次向上移动,虽然有滚动到列表的底部,但是焦点丢失了。
分析:通过log发现,第二次向上移动时,最后一个item没有跑Adapter的onBindViewHolder函数,导致onBindViewHoler中设置焦点的代码没有跑
if (selectedPosition == position) {
holder.container.requestFocus();
}
再分析log发现,第一次打开列表,会跑11次onBindViewHoler函数(page size为8),第一次移动到底部时onBindViewHoler也跑了11次,但第二次就只有8次了,最后的三个item没有跑onBindViewHoler来加载。
结论:RecyclerView自带的缓存机制导致这种问题,RecyclerView会预加载前后一定数量的item,这些item在显示时不会再次调用onBindViewHoler
对策:总之这是由于RecyclerView本身的机制导致,那么找一个item显示时一定会跑的方法来设置焦点即可
@Override
public void onViewAttachedToWindow(MyViewHolder holder) {
super.onViewAttachedToWindow(holder);
MyLinearLayoutManager ll = (MyLinearLayoutManager) mRecyclerView.getLayoutManager();
int position = ll.getPosition(holder.container);
if (needSetSelect && position == selectedPosition && !holder.container.isFocused()) {
holder.container.requestFocus();
needSetSelect = false;
}
}
public void setSelection(int position) {
selectedPosition = position;
needSetSelect = true;
}
Item显示一定会跑onViewAttachedToWindow方法,在这里再设置焦点即可。
补充1:
之前测试漏了一种情况,当前pageSize=8,如果list只有10项,初始显示范围0-7,翻页后为2-9,焦点在2上,但是ITEM 2在视图1中已经显示了,所以既不会跑onBindViewHoler,也不会跑onViewAttachedToWindow,所以又会出现焦点丢失的问题
直接给对策,重写LinearLayoutManager,在onInterceptFocusSearch中处理焦点问题:
@Override
public View onInterceptFocusSearch(View focused, int direction) {
if (mRecyclerView != null) {
int count = getItemCount();
int fromPos = getPosition(focused);
int firtVisibleItemPos = findFirstVisibleItemPosition();
int lastVisibleItemPos = findLastVisibleItemPosition();
int lastPos = 0;
MyAdapter adapter = (MyAdapter) mRecyclerView.getAdapter();
switch (direction) {
case View.FOCUS_RIGHT:
if (count > SIZE_PAGE) {
fromPos = firtVisibleItemPos + SIZE_PAGE;
lastPos = lastVisibleItemPos + SIZE_PAGE;
if (fromPos >= count) {
fromPos = 0;
} else if (lastPos >= count) {
fromPos = count - SIZE_PAGE;
}
scrollToPositionWithOffset(fromPos, 0);
// 如果焦点在上一个视图内,用getChildAt来获取ITEM,并使其获取焦点
if (fromPos <= lastVisibleItemPos
&& fromPos >= firtVisibleItemPos) {
mRecyclerView.getChildAt(fromPos - firtVisibleItemPos)
.requestFocus();
break;
}
adapter.setSelection(fromPos);
}
break;
case View.FOCUS_LEFT:
if (count > SIZE_PAGE) {
fromPos = firtVisibleItemPos - SIZE_PAGE;
lastPos = lastVisibleItemPos - SIZE_PAGE;
if (lastPos < 0) {
fromPos = count - SIZE_PAGE;
} else if (fromPos < 0) {
fromPos = 0;
}
scrollToPositionWithOffset(fromPos, 0);
// 如果焦点在上一个视图内,用getChildAt来获取ITEM,并使其获取焦点
if (fromPos <= lastVisibleItemPos
&& fromPos >= firtVisibleItemPos) {
mRecyclerView.getChildAt(fromPos - firtVisibleItemPos)
.requestFocus();
break;
}
adapter.setSelection(fromPos);
}
break;
case View.FOCUS_DOWN:
if (fromPos < firtVisibleItemPos
|| fromPos > lastVisibleItemPos) {
return focused;
}
fromPos++;
if (fromPos >= count) {
fromPos = 0;
scrollToPositionWithOffset(fromPos, 0);
// 焦点跳转时清除焦点
focused.clearFocus();
adapter.setSelection(fromPos);
}
break;
case View.FOCUS_UP:
if (fromPos < firtVisibleItemPos
|| fromPos > lastVisibleItemPos) {
return focused;
}
fromPos--;
if (fromPos < 0) {
fromPos = count - 1;
scrollToPositionWithOffset(fromPos, SIZE_PAGE - 1);
// 焦点跳转时清除焦点
focused.clearFocus();
adapter.setSelection(fromPos);
}
break;
}
}
return super.onInterceptFocusSearch(focused, direction);
}
总之:
当焦点不可见时:在Adapter的onViewAttachedToWindow中设置焦点
当焦点可见时:在onInterceptFocusSearch中设置焦点