Android TV開發總結【RecycleView】
阿新 • • 發佈:2019-01-30
在TV開發中RecycleView的使用是最讓人頭疼的經常會出現焦點丟失。因為當item未顯示時不能獲取焦點。所以當我們按上下鍵時經常丟失焦點或者焦點亂跳。要解決這個問題我們必須要手動控制RecyclerView 的按鍵和焦點移動。
所以我們這裡需要需要自定義RecycleView。
程式碼如下,各個方法作用在注視中已新增:
public class TvRecyclerView extends RecyclerView
{
//正常跟隨滾動
private static final int SCROLL_NORMAL = 0;
//居中滾動
private static final int SCROLL_FOLLOW = 1;
//滾動模式
private int scrollModel;
//當前選中的position
private int mSelectedPosition = 0;
//下一個聚焦的View
private View mNextFocused;
public TvRecyclerViewNew(Context context)
{
this(context, null);
}
public TvRecyclerViewNew(Context context, AttributeSet attrs)
{
this (context, attrs, -1);
}
public TvRecyclerViewNew(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
/**
* 初始化
*
* @param context
* @param attrs
* @param defStyle
*/
private void init(Context context, AttributeSet attrs, int defStyle)
{
initView();
initAttr(attrs);
}
/**
* 初始化View
* 為避免recycleview焦點混亂常用的一些設定
*/
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);
}
/**
* 初始化樣式
* 是否居中滾動
* @param attrs
*/
private void initAttr(AttributeSet attrs)
{
TypedArray typeArray = getContext().obtainStyledAttributes(attrs, R.styleable.TvRecyclerView);
scrollModel = typeArray.getInteger(R.styleable.TvRecyclerView_scrollMode, 0);
}
/**
* 恢復回收之前的狀態
* @param state
*/
@Override
protected void onRestoreInstanceState(Parcelable state)
{
Bundle bundle = (Bundle) state;
Parcelable superData = bundle.getParcelable("super_data");
super.onRestoreInstanceState(superData);
setItemSelected(bundle.getInt("select_pos", 0));
}
/**
* 回收之前儲存狀態
* @return
*/
@Override
protected Parcelable onSaveInstanceState()
{
Bundle bundle = new Bundle();
Parcelable superData = super.onSaveInstanceState();
bundle.putParcelable("super_data", superData);
bundle.putInt("select_pos", mSelectedPosition);
return bundle;
}
/**
* 解決4.4版本搶焦點的問題
* @return
*/
@Override
public boolean isInTouchMode()
{
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;
}
/**
* 判斷是垂直,還是橫向.
*/
private boolean isVertical()
{
LayoutManager manager = getLayoutManager();
if (manager != null)
{
LinearLayoutManager layout = (LinearLayoutManager) getLayoutManager();
return layout.getOrientation() == LinearLayoutManager.VERTICAL;
}
return false;
}
/**
* 滾動的相關響應
* computeScroll在父控制元件執行drawChild時,會呼叫這個方法
*/
@Override
public void computeScroll()
{
super.computeScroll();
//滾動後更新當前選中的position
if (mNextFocused != null)
{
mSelectedPosition = getChildAdapterPosition(mNextFocused);
} else
{
mSelectedPosition = getChildAdapterPosition(getFocusedChild());
}
}
/**
* 返回迭代的繪製子類索引。如果你想改變子類的繪製順序就要重寫該方法
* 提示:為了能夠呼叫該方法,你必須首先呼叫setChildrenDrawingOrderEnabled(boolean)來允許子類排序
*
* @param childCount 子類個數
* @param i 當前迭代順序
* @return 繪製該迭代子類的索引
*/
@Override
protected int getChildDrawingOrder(int childCount, int i)
{
View view = getFocusedChild();
if (null != view)
{
int position = getChildAdapterPosition(view) - getFirstVisiblePosition();
if (position < 0)
{
return i;
} else
{
if (i == childCount - 1)
{
if (position > i)
{
position = i;
}
return position;
}
if (i == position)
{
return childCount - 1;
}
}
}
return i;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event)
{
boolean result = super.dispatchKeyEvent(event);
View focusView = this.getFocusedChild();
if (focusView == null)
{
return result;
} else
{
if (event.getAction() == KeyEvent.ACTION_UP)
{
//不能攔截KeyEvent.KEYCODE_BACK
//否則onBackPress不會觸發
if(event.getKeyCode() == KeyEvent.KEYCODE_BACK){
return super.dispatchKeyEvent(event);
}else {
return true;
}
} else
{
switch (event.getKeyCode())
{
case KeyEvent.KEYCODE_DPAD_RIGHT:
View rightView = mNextFocused = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_RIGHT);
setViewPosition(mNextFocused);
if (rightView != null)
{
rightView.requestFocus();
return true;
} else
{
return false;
}
case KeyEvent.KEYCODE_DPAD_LEFT:
View leftView = mNextFocused = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_LEFT);
setViewPosition(mNextFocused);
if (leftView != null)
{
mSelectedPosition = getChildAdapterPosition(leftView);
} else
{
mSelectedPosition = getChildAdapterPosition(getFocusedChild());
}
if (leftView != null)
{
leftView.requestFocus();
return true;
} else
{
return false;
}
case KeyEvent.KEYCODE_DPAD_DOWN:
View downView = mNextFocused = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_DOWN);
setViewPosition(mNextFocused);
if (downView != null)
{
downView.requestFocus();
if (scrollModel == SCROLL_NORMAL)
{
//跟隨滾動直接返回true
return true;
} else
{
//居中滾動計算出滾動距離,將view滾動到中間
int downOffset = downView.getTop() + downView.getHeight() / 2 - getHeight() / 2;
this.smoothScrollBy(0, downOffset);
return true;
}
} else
{
return isBottomEdge(getLayoutManager().getPosition(this.getFocusedChild()));
}
case KeyEvent.KEYCODE_DPAD_UP:
View upView = mNextFocused = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_UP);
setViewPosition(mNextFocused);
if (upView != null)
{
upView.requestFocus();
if (scrollModel == SCROLL_NORMAL)
{
return true;
} else
{
int upOffset = getHeight() / 2 - (upView.getBottom() - upView.getHeight() / 2);
this.smoothScrollBy(0, -upOffset);
return true;
}
} else
{
return isTopEdge(getLayoutManager().getPosition(this.getFocusedChild())) ;
}
}
}
}
return result;
}
private void setViewPosition(View mNextFocused){
if(mNextFocused != null){
mSelectedPosition = getChildAdapterPosition(mNextFocused);
}else {
mSelectedPosition = getChildAdapterPosition(getFocusedChild());
}
}
//防止Activity時,RecyclerView崩潰
@Override
protected void onDetachedFromWindow()
{
if (getLayoutManager() != null)
{
super.onDetachedFromWindow();
}
}
/**
* 設定選中的item
* @param position
*/
public void setItemSelected(int position)
{
if (mSelectedPosition == position)
{
return;
}
if (position >= getAdapter().getItemCount())
{
position = getAdapter().getItemCount() - 1;
}
mSelectedPosition = position;
requestLayout();
}
/**
* 是否是最右邊的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;
}
/**
* 判斷是否已經滑動到底部
*
* @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;
}
}
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));
}
private int getFreeWidth()
{
return getWidth() - getPaddingLeft() - getPaddingRight();
}
private int getFreeHeight()
{
return getHeight() - getPaddingTop() - getPaddingBottom();
}
public int getSelectedPosition()
{
return mSelectedPosition;
}
public void setSelectionPostion(int selectionPostion)
{
mSelectedPosition = selectionPostion;
}
}
最後一點不要忘記在attrs.xml
中新增TvRecycelview樣式:
<!--TvRecycelvie滾動-->
<attr name="scrollMode" >
<enum name="normalScroll" value="0"/>
<enum name="followScroll" value="1"/>
</attr>
<!--TvRecycelview樣式-->
<declare-styleable name="TvRecyclerView">
<attr name="scrollMode"/>
</declare-styleable>