原生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直接获取焦点。不再依赖系统处理。
BaseFragment 为与之绑定的fragment,作用是当按上键离开fragment到导航栏时的焦点位置。实现记录焦点功能。