前言
最近,在做一个菜单功能,其实就是一个RecyclerView的列表,需要做循环。因为在TV上,涉及焦点问题,所以跟手机还是有些许不同,遇到了一些问题,网上只有一篇相关,所以,完成了功能之后,自己来总结分享一下。
解决方案
第一种:
在adapter里面设置第一个和最后一个view的key监听事件,scrollToPosition,然后给recyclerView添加OnScrollListener,然后根据不同的方向,选择第一个还是最后一个view来请求焦点。
这种是参看Android TV中实现RecyView循环功能,不过也是有点问题,如果recyclerview没有滚动的话就有问题。(解决呢就是,判断一下是否能滚动,也就是子view的总高度是否大于recycler的高度,是滚动之后requestFocus,还是直接requestFocus)
第二种:(推荐,最简单的一种)
在adapter里面设置第一个和最后一个view的key监听事件,然后scrollToPosition,然后requestFocus,不过这里需要延迟执行
public class MenuMainAdapter extends RecyclerView.Adapter<MenuMainAdapter.ViewHolder> {
private OnCircleListener mOnCircleListener;
@Override
public void onBindViewHolder(@NonNull MenuMainAdapter.ViewHolder holder, int position) {
...
if (mOnCircleListener != null) {
holder.itemView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (holder.getAdapterPosition() == 0) {
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
mOnCircleListener.onUpKey();
return true;
}
}
} else if (holder.getAdapterPosition() == data.size() - 1) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
mOnCircleListener.onDownKey();
return true;
}
}
}
return false;
}
});
}
}
public interface OnCircleListener {
void onUpKey();
void onDownKey();
}
public void setOnCircleListener(OnCircleListener mOnCircleListener) {
this.mOnCircleListener = mOnCircleListener;
}
adapter.setOnCircleListener(new MenuMainAdapter.OnCircleListener() {
@Override
public void onUpKey() {
recyclerView.scrollToPosition(adapter.getItemCount() - 1);
getSafetyHandler().postDelayed(new Runnable() {
@Override
public void run() {
requestFocus(adapter.getItemCount() - 1);
}
}, 100);
}
@Override
public void onDownKey() {
recyclerView.scrollToPosition(0);
getSafetyHandler().postDelayed(new Runnable() {
@Override
public void run() {
requestFocus(0);
}
}, 100);
}
});
public void requestFocus(int position) {
View view = recyclerView.getChildAt(position);
LinearLayoutManager llM = (LinearLayoutManager) recyclerView.getLayoutManager();
if (view != null) {
view.requestFocus();
} else if (llM.findViewByPosition(position) != null) {
llM.findViewByPosition(position).requestFocus();
} else {
recyclerView.requestFocus();
}
}
过程分析
看到上面的第二种解决方法里的requestFocus方法
public void requestFocus(int position) {
View view = recyclerView.getChildAt(position);
LinearLayoutManager llM = (LinearLayoutManager) recyclerView.getLayoutManager();
if (view != null) {
view.requestFocus();
} else if (llM.findViewByPosition(position) != null) {
llM.findViewByPosition(position).requestFocus();
} else {
recyclerView.requestFocus();
}
}
为什么要延迟100毫秒?
原因:
首先我们先说几个注意的问题:
1、recyclerView的childrenCount个数跟adapter的data个数不一定是相等的(因为复用)
2、getChildAt使用adapter的position是不可靠的,可能获取到的为null,如果data有100个,一屏幕只能显示10个,那么childrenCount只有10个。如果超过了,自然获取为null
3、layoutManager的findViewByPosition方法也是不可靠的,可能获取到的为null,可以看一下源码
public View findViewByPosition(int position) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
ViewHolder vh = getChildViewHolderInt(child);
if (vh == null) {
continue;
}
if (vh.getLayoutPosition() == position && !vh.shouldIgnore()
&& (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) {
return child;
}
}
return null;
}
这里可以看出,它是从childView里面去找现在显示的子view绑定的holder是否跟position相等,如果是才返回,不然返回null。所以,如果position对应的view如果没有显示出来(或者说被自己绑定在使用)就返回null。
我们延迟100毫秒,就是想要保证对应position的view(比如最后一个),已经显示在界面上了,然后再去让它requestFocus(position),才能找到它本身绑定的view,进行requesFocus,才能够正常显示。这个100毫秒,也可以自行调测,找出一个完美的值。
看第一种方法里面,为啥要给recyclerView添加onScrollListener并且在IDLE的时候去处理请求focus,就是想要在滚动完毕之后再去请求,但是操作有点复杂了。
最后还要注意一点:
有些人想要使用recycler的SmoothScrollToPotion,这个方法呢,也可以用,如果你的遥控器操作不会一直按着,然后让recycler飞快地滚动这种情况呢,是可以用的。
如果是有的话,那么使用起来就会有一些小问题,会出现乱滚的现象,因为smooth的滚动,不是即时的,是会有一定时间的,所以,这个时间差就会导致代码调用出现一些问题。