Android TV中RecyclerView循环切换

前言

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

    原文作者:Jafir
    原文地址: https://www.jianshu.com/p/e2a1f818633e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞