Android TV中RecyclerView循环切换

    技术2022-07-17  95

    前言

    最近,在做一个菜单功能,其实就是一个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的滚动,不是即时的,是会有一定时间的,所以,这个时间差就会导致代码调用出现一些问题。

    Processed: 0.012, SQL: 9