1. 程式人生 > >Android TV開發總結【RecycleView】

Android TV開發總結【RecycleView】

在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>