原生RecyclerView 在Tv中的焦点处理很不好,经常找不到焦点或者焦点丢失。原因是因为当item未显示时即未加载时时不能获取焦点的。所以当我们按上下键时经常丢失焦点或者焦点乱跳。要解决这个问题我们必须要手动控制RecyclerView 的按键和焦点移动。
package com.phicomm.ottbox.view;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import com.phicomm.ottbox.base.BaseFragment;
/**
- Created by root on 17-9-7.
*/
public class TvRecyclerView extends RecyclerView {
private static final String TAG = “TvRecyclerView”;
private int mPosition; private BaseFragment mBindFragment; public TvRecyclerView(Context context) { this(context, null); } public TvRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, -1); } public TvRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs, defStyle); } private void init(Context context, AttributeSet attrs, int defStyle) { initView(); //initAttr(context, attrs); } public void setBindFragment(BaseFragment fragment) { this.mBindFragment = fragment; } private void initView() { setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setHasFixedSize(true); setWillNotDraw(true); setOverScrollMode(View.OVER_SCROLL_NEVER); setChildrenDrawingOrderEnabled(true); setClipChildren(false); setClipToPadding(false); setClickable(false); setFocusable(true); setFocusableInTouchMode(true); /** 防止RecyclerView刷新时焦点不错乱bug的步骤如下: (1)adapter执行setHasStableIds(true)方法 (2)重写getItemId()方法,让每个view都有各自的id (3)RecyclerView的动画必须去掉 */ setItemAnimator(null); } private int getFreeWidth() { return getWidth() - getPaddingLeft() - getPaddingRight(); } private int getFreeHeight() { return getHeight() - getPaddingTop() - getPaddingBottom(); } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } @Override public boolean hasFocus() { return super.hasFocus(); } @Override public boolean isInTouchMode() { // 解决4.4版本抢焦点的问题 if (Build.VERSION.SDK_INT == 19) { return !(hasFocus() && !super.isInTouchMode()); } else { return super.isInTouchMode(); } } @Override public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); } @Override public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { final int parentLeft = getPaddingLeft(); final int parentRight = getWidth() - getPaddingRight(); final int parentTop = getPaddingTop(); final int parentBottom = getHeight() - getPaddingBottom(); final int childLeft = child.getLeft() + rect.left; final int childTop = child.getTop() + rect.top; final int childRight = childLeft + rect.width(); final int childBottom = childTop + rect.height(); final int offScreenLeft = Math.min(0, childLeft - parentLeft); final int offScreenRight = Math.max(0, childRight - parentRight); final int offScreenTop = Math.min(0, childTop - parentTop); final int offScreenBottom = Math.max(0, childBottom - parentBottom); final boolean canScrollHorizontal = getLayoutManager().canScrollHorizontally(); final boolean canScrollVertical = getLayoutManager().canScrollVertically(); // Favor the "start" layout direction over the end when bringing one side or the other // of a large rect into view. If we decide to bring in end because start is already // visible, limit the scroll such that start won't go out of bounds. final int dx; if (canScrollHorizontal) { if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) { dx = offScreenRight != 0 ? offScreenRight : Math.max(offScreenLeft, childRight - parentRight); } else { dx = offScreenLeft != 0 ? offScreenLeft : Math.min(childLeft - parentLeft, offScreenRight); } } else { dx = 0; } // Favor bringing the top into view over the bottom. If top is already visible and // we should scroll to make bottom visible, make sure top does not go out of bounds. final int dy; if (canScrollVertical) { dy = offScreenTop != 0 ? offScreenTop : Math.min(childTop - parentTop, offScreenBottom); } else { dy = 0; } if (dx != 0 || dy != 0) { if (immediate) { scrollBy(dx, dy); } else { smoothScrollBy(dx, dy); } postInvalidate(); return true; } return false; } @Override public int getBaseline() { return -1; } @Override public void setLayoutManager(LayoutManager layout) { super.setLayoutManager(layout); } /** * 判断是垂直,还是横向. */ private boolean isVertical() { LayoutManager manager = getLayoutManager(); if (manager != null) { LinearLayoutManager layout = (LinearLayoutManager) getLayoutManager(); return layout.getOrientation() == LinearLayoutManager.VERTICAL; } return false; } @Override protected int getChildDrawingOrder(int childCount, int i) { View view = getFocusedChild(); if (null != view) { mPosition = getChildAdapterPosition(view) - getFirstVisiblePosition(); if (mPosition < 0) { return i; } else { if (i == childCount - 1) { if (mPosition > i) { mPosition = i; } return mPosition; } if (i == mPosition) { return childCount - 1; } } } return i; } public int getFirstVisiblePosition() { if (getChildCount() == 0) return 0; else return getChildAdapterPosition(getChildAt(0)); } public int getLastVisiblePosition() { final int childCount = getChildCount(); if (childCount == 0) return 0; else return getChildAdapterPosition(getChildAt(childCount - 1)); } /** * 设置为0,这样可以防止View获取焦点的时候,ScrollView自动滚动到焦点View的位置 */ protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { return 0; } @Override public boolean dispatchKeyEvent(KeyEvent event) { boolean result = super.dispatchKeyEvent(event); View focusView = this.getFocusedChild(); if (mBindFragment != null) { mBindFragment.setCacheViewFromContent(focusView); } if (focusView == null) { return result; } else { if (event.getAction() == KeyEvent.ACTION_UP) { return true; } else { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_RIGHT: View rightView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_RIGHT); if (rightView != null) { rightView.requestFocus(); return true; } else { return false; } case KeyEvent.KEYCODE_DPAD_LEFT: View leftView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_LEFT); Log.i(TAG, "leftView is null:" + (leftView == null)); if (leftView != null) { leftView.requestFocus(); return true; } else { return false; } case KeyEvent.KEYCODE_DPAD_DOWN: if (isVisBottom(this)) { this.smoothScrollToPosition(getLastVisiblePosition()); return result; } View downView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_DOWN); Log.i(TAG, " downView is null:" + (downView == null)); if (downView != null) { downView.requestFocus(); int downOffset = downView.getTop() + downView.getHeight() / 2 - getHeight() / 2; this.smoothScrollBy(0, downOffset); return true; } else { return true; } case KeyEvent.KEYCODE_DPAD_UP: View upView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_UP); Log.i(TAG, "upView is null:" + (upView == null)); if (upView != null) { upView.requestFocus(); int upOffset = getHeight() / 2 - (upView.getBottom() - upView.getHeight() / 2); this.smoothScrollBy(0, -upOffset); return true; } else { Log.i(TAG, "tab cache view"); if (mBindFragment != null) { mBindFragment.setmCacheViewFromTab(focusView); } return result;//返回false,否则第一行按上键回不到导航栏 } } } } return result; } @Override public boolean onInterceptTouchEvent(MotionEvent e) { return super.onInterceptTouchEvent(e); } //防止Activity时,RecyclerView崩溃 @Override protected void onDetachedFromWindow() { if (getLayoutManager() != null) { super.onDetachedFromWindow(); } } /** * 是否是最右边的item,如果是竖向,表示右边,如果是横向表示下边 * * @param childPosition * @return */ public boolean isRightEdge(int childPosition) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; GridLayoutManager.SpanSizeLookup spanSizeLookUp = gridLayoutManager.getSpanSizeLookup(); int totalSpanCount = gridLayoutManager.getSpanCount(); int totalItemCount = gridLayoutManager.getItemCount(); int childSpanCount = 0; for (int i = 0; i <= childPosition; i++) { childSpanCount += spanSizeLookUp.getSpanSize(i); } if (isVertical()) { if (childSpanCount % gridLayoutManager.getSpanCount() == 0) { return true; } } else { int lastColumnSize = totalItemCount % totalSpanCount; if (lastColumnSize == 0) { lastColumnSize = totalSpanCount; } if (childSpanCount > totalItemCount - lastColumnSize) { return true; } } } else if (layoutManager instanceof LinearLayoutManager) { if (isVertical()) { return true; } else { return childPosition == getLayoutManager().getItemCount() - 1; } } return false; } /** * 是否是最左边的item,如果是竖向,表示左方,如果是横向,表示上边 * * @param childPosition * @return */ public boolean isLeftEdge(int childPosition) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; GridLayoutManager.SpanSizeLookup spanSizeLookUp = gridLayoutManager.getSpanSizeLookup(); int totalSpanCount = gridLayoutManager.getSpanCount(); int childSpanCount = 0; for (int i = 0; i <= childPosition; i++) { childSpanCount += spanSizeLookUp.getSpanSize(i); } if (isVertical()) { if (childSpanCount % gridLayoutManager.getSpanCount() == 1) { return true; } } else { if (childSpanCount <= totalSpanCount) { return true; } } } else if (layoutManager instanceof LinearLayoutManager) { if (isVertical()) { return true; } else { return childPosition == 0; } } return false; } /** * 是否是最上边的item,以recyclerview的方向做参考 * * @param childPosition * @return */ public boolean isTopEdge(int childPosition) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; GridLayoutManager.SpanSizeLookup spanSizeLookUp = gridLayoutManager.getSpanSizeLookup(); int totalSpanCount = gridLayoutManager.getSpanCount(); int childSpanCount = 0; for (int i = 0; i <= childPosition; i++) { childSpanCount += spanSizeLookUp.getSpanSize(i); } if (isVertical()) { if (childSpanCount <= totalSpanCount) { return true; } } else { if (childSpanCount % totalSpanCount == 1) { return true; } } } else if (layoutManager instanceof LinearLayoutManager) { if (isVertical()) { return childPosition == 0; } else { return true; } } return false; } /** * 是否是最下边的item,以recyclerview的方向做参考 * * @param childPosition * @return */ public boolean isBottomEdge(int childPosition) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; GridLayoutManager.SpanSizeLookup spanSizeLookUp = gridLayoutManager.getSpanSizeLookup(); int itemCount = gridLayoutManager.getItemCount(); int childSpanCount = 0; int totalSpanCount = gridLayoutManager.getSpanCount(); for (int i = 0; i <= childPosition; i++) { childSpanCount += spanSizeLookUp.getSpanSize(i); } if (isVertical()) { //最后一行item的个数 int lastRowCount = itemCount % totalSpanCount; if (lastRowCount == 0) { lastRowCount = gridLayoutManager.getSpanCount(); } if (childSpanCount > itemCount - lastRowCount) { return true; } } else { if (childSpanCount % totalSpanCount == 0) { return true; } } } else if (layoutManager instanceof LinearLayoutManager) { if (isVertical()) { return childPosition == getLayoutManager().getItemCount() - 1; } else { return true; } } return false; } public interface OnInterceptListener { boolean onIntercept(KeyEvent event); } /** * 判断是否已经滑动到底部 * * @param recyclerView * @return */ private boolean isVisBottom(RecyclerView recyclerView) { LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); int visibleItemCount = layoutManager.getChildCount(); int totalItemCount = layoutManager.getItemCount(); if (visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1) { return true; } else { return false; } }
}
重点是在dispatchKeyEvent中,让我们下一个item直接获取焦点。不再依赖系统处理。