Android TV開發總結【RecycleView】

先知先覺發表於2018-08-01

在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>
複製程式碼

相關文章