TV端焦点乱飞一直是很头疼的事情,现在给大家推荐一个不需要限速的TvRecyclerView简单好用,让你的TV端告别卡顿.还能有效减少在低配置机型上ANR的几率.

 

tv端焦点移动限速代码,某些特定条件下需要加上限速的朋友可以用以下代码,在BaseActivity中加上

private long mLastKeyDownTime = 0;
/**
 * 限速时间,可按需求增加或减少
 */
public int mTimeIntervalBetween = 150;
@Override
public boolean dispatchKeyEvent (KeyEvent event){

    //兼容手柄确定键。
    if(event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A){
        return super.dispatchKeyEvent (event);
    }

    if (event.getAction () == KeyEvent.ACTION_DOWN){
        long current = System.currentTimeMillis ();
        boolean res;
        if (current - mLastKeyDownTime < mTimeIntervalBetween ){
            res = true;
            return res;
        } else{
            // TODO: 2019/6/13 有操作后将无操作时间重置为0
            mNoOperatingTime = 0;

            mLastKeyDownTime = current;
            boolean b = rewriteOnKeyDown (event.getKeyCode () , event);
            if (b){
                return true;
            } else{
                return super.dispatchKeyEvent (event);
            }
        }
    }
    return super.dispatchKeyEvent (event);
}

接下来是RecyclerView的代码,用此替换系统的RecyclerView可以让你在RecyclerView不管怎么飞速滑动都不会丢失焦点,让你体验飞一般的丝滑.还能让你当前获得焦点的那个居中显示.不需要的朋友可以把requestChildRectangleOnScreen方法注释掉就行.

 

废话不多说直接上代码

 

 

package com.hulian.newos.viw

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;

import com.blankj.utilcode.util.LogUtils;
import com.hulian.newos.Const;

import java.util.ArrayList;

/**
 * ================================================
 *
 * @author 作    者:
 * 版    本:1.0
 * 创建日期:2020/1/2
 * 描    述:
 * 修订历史:
 * ================================================
 */
public class TvXRecyclerView extends RecyclerView implements View.OnFocusChangeListener{

    private int mSelectedPosition = 0;

    private int mSelectedItemOffsetStart;

    private int mSelectedItemOffsetEnd;
    private View mChild;
    /**
     * 滑动次数
     */
    public int mScrollNumber = 0;
    /**
     * 是否在滑动中
     */
    public boolean mIsScrolling;
    /**
     * 最左边的view是否消耗掉按键,屏幕的上下左右
     */
    private boolean mIsHandLastLeftKey = true, mIsHandLastRightKey = true, mIsHandLastUpKey = true, mIsHandLastDownKey = true;

//    /**
//     * 快速滑动不获焦开关
//     */
//    private boolean mIsLongPressDoesNotGainFocus = false;
    /**
     * 按钮事件
     */
    private int mKeyEventCode;

    /**
     * 左右滑动的时候不进行rv拖动,默认为进行居中滑动
     */
    private boolean mLeftRightNoScroll = false;
    /**
     * 子adapter当前选中的position
     */
    private int mChildAdapterPosition;
    /**
     * 设置居中偏移量
     */
    private int mDyOffset;
    private final Rect mTempRect = new Rect ();

    /**
     * 设置居中偏移量
     *
     * @param dyOffset
     */
    public void setDyOffset (int dyOffset){
        mDyOffset = dyOffset;
    }

    public int getChildAdapterPosition (){
        return mChildAdapterPosition;
    }

//    public void setLongPressDoesNotGainFocus (boolean longPressDoesNotGainFocus){
//        mIsLongPressDoesNotGainFocus = longPressDoesNotGainFocus;
//    }

    public void setLeftRightNoScroll (boolean leftRightNoScroll){
        mLeftRightNoScroll = leftRightNoScroll;
    }

    /**
     * 是否拦截住不让出去
     * @param mIsHandLastLeftKey
     * @param mIsHandLastRightKey
     * @param mIsHandLastUpKey
     * @param mIsHandLastDownKey
     */
    public void setHandLastKey (boolean mIsHandLastLeftKey , boolean mIsHandLastRightKey , boolean mIsHandLastUpKey , boolean mIsHandLastDownKey){
        this.mIsHandLastLeftKey = mIsHandLastLeftKey;
        this.mIsHandLastRightKey = mIsHandLastRightKey;
        this.mIsHandLastUpKey = mIsHandLastUpKey;
        this.mIsHandLastDownKey = mIsHandLastDownKey;
    }

    public TvXRecyclerView (Context context){
        super (context);
        init ();
    }

    public TvXRecyclerView (Context context , @Nullable AttributeSet attrs){
        super (context , attrs);
        init ();
    }

    public TvXRecyclerView (Context context , @Nullable AttributeSet attrs , int defStyle){
        super (context , attrs , defStyle);
        init ();
    }

    private void init (){

        setPreserveFocusAfterLayout (false);
        setDescendantFocusability (FOCUS_AFTER_DESCENDANTS);
        setChildrenDrawingOrderEnabled (true);
        // 自身不作onDraw处理
        setWillNotDraw (true);
        setHasFixedSize (true);
        setOverScrollMode (View.OVER_SCROLL_NEVER);

        setClipChildren (false);
        setClipToPadding (false);
        setFocusable (true);
        setFocusableInTouchMode (true);

        //启用子视图排序功能
        //取消item动画
        setItemAnimator (null);
    }

    @Override
    public void requestChildFocus (View child , View focused){
        if (null != child){
            mChild = child;
            mSelectedItemOffsetStart = ! isVertical () ? (getFreeWidth () - child.getWidth ()) : (getFreeHeight () - child.getHeight ());
            mSelectedItemOffsetStart /= 2;
            mSelectedItemOffsetEnd = mSelectedItemOffsetStart;
        }
        super.requestChildFocus (child , focused);

    }

    @Override
    public void addFocusables (ArrayList <View> views , int direction , int focusableMode){

        View view = getLayoutManager ().findViewByPosition (mChildAdapterPosition);
        if (hasFocus () || mChildAdapterPosition < 0 || view == null){
            super.addFocusables (views , direction , focusableMode);
        } else if (view.isFocusable ()){
            //将当前的view放到Focusable views列表中,再次移入焦点时会取到该view,实现焦点记忆功能
            //实际上使用用处有但是没有想象中的好,也可以用
            LogUtils.e ("实现焦点记忆功能" , views.size () , direction , focusableMode);
            views.add (view);
        } else{
            super.addFocusables (views , direction , focusableMode);
        }
    }

    private int getFreeWidth (){
        return getWidth () - getPaddingLeft () - getPaddingRight ();
    }

    private int getFreeHeight (){
        return getHeight () - getPaddingTop () - getPaddingBottom ();
    }

    @Override
    protected int getChildDrawingOrder (int childCount , int i){

        int position = mSelectedPosition;
        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 void onDraw (Canvas c){
// mSelectedPosition = getChildAdapterPosition(getFocusedChild());

        mSelectedPosition = indexOfChild (getFocusedChild ());
        super.onDraw (c);
    }

    @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 - mSelectedItemOffsetStart);
        final int offScreenRight = Math.max (0 , childRight - parentRight + mSelectedItemOffsetEnd);

        final int offScreenTop = Math.min (0 , childTop - parentTop - mSelectedItemOffsetStart);
        final int offScreenBottom = Math.max (0 , childBottom - parentBottom + mSelectedItemOffsetEnd);


        final boolean canScrollHorizontal = getLayoutManager ().canScrollHorizontally ();
        final boolean canScrollVertical = getLayoutManager ().canScrollVertically ();


        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;
        }

        int dy;
        if (canScrollVertical){
            dy = offScreenTop != 0 ? offScreenTop : Math.min (childTop - parentTop , offScreenBottom);
        } else{
            dy = 0;
        }

        try{
            if ((dx != 0 || dy != 0)){
                if (mLeftRightNoScroll && (mKeyEventCode == KeyEvent.KEYCODE_DPAD_LEFT || mKeyEventCode == KeyEvent.KEYCODE_DPAD_RIGHT)){
                    return true;
                } else{
                    mScrollNumber++;
                    if (immediate){
                        scrollBy (dx , dy + mDyOffset);
                    } else{
                        smoothScrollBy (dx , dy + mDyOffset , new DecelerateInterpolator (1.5f));
                    }
                }
                // 重绘
                // .是为了选中item置顶,具体请参考getChildDrawingOrder方法
                postInvalidate ();
                return true;
            }

        } catch (Exception e){
            e.printStackTrace ();
        }

        return false;
    }

    /**
     * 判断是垂直,还是横向.
     */
    private boolean isVertical (){
        LayoutManager manager = getLayoutManager ();
        if (manager != null){
            LinearLayoutManager layout = (LinearLayoutManager) getLayoutManager ();
            return layout.getOrientation () == LinearLayoutManager.VERTICAL;

        }
        return false;
    }


    @Override
    public boolean dispatchKeyEvent (KeyEvent event){
        try{
            int keyCode = event.getKeyCode ();
            mKeyEventCode = keyCode;
           
            


            View mVideoFocusView = getFocusedChild ();
            mChildAdapterPosition = getLayoutManager ().getPosition (mVideoFocusView);
            LogUtils.e ("mChildAdapterPosition" + mChildAdapterPosition);

            if (event.getAction () == KeyEvent.ACTION_DOWN){
                if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
                        || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP){
                    View nextFocusView = null;
                    try{
                        switch (keyCode){
                            case KeyEvent.KEYCODE_DPAD_LEFT:
                                if (mIsHandLastLeftKey){
                                    nextFocusView = FocusFinder.getInstance ().findNextFocus (this , findFocus () , View.FOCUS_LEFT);
                                    if (nextFocusView == null){
                                        LogUtils.e ("keyEvent拦截======左");
                                        return true;
                                    }
                                }
                                break;
                            case KeyEvent.KEYCODE_DPAD_RIGHT:
                                if (mIsHandLastRightKey){
                                    nextFocusView = FocusFinder.getInstance ().findNextFocus (this , findFocus () , View.FOCUS_RIGHT);
                                    if (nextFocusView == null){
                                        LogUtils.e ("keyEvent拦截======右");
                                        return true;
                                    }
                                }
                                break;
                            case KeyEvent.KEYCODE_DPAD_DOWN:
                                if (mIsHandLastDownKey){
                                    nextFocusView = FocusFinder.getInstance ().findNextFocus (this , findFocus () , View.FOCUS_DOWN);
                                    if (nextFocusView == null){
                                        LogUtils.e ("keyEvent拦截======下");
                                        return true;
                                    }
                                }
                                break;
                            case KeyEvent.KEYCODE_DPAD_UP:
                                if (mIsHandLastUpKey){
                                    nextFocusView = FocusFinder.getInstance ().findNextFocus (this , findFocus () , View.FOCUS_UP);
                                    if (nextFocusView == null){
                                        LogUtils.e ("keyEvent拦截======上");
                                        return true;
                                    }
                                }
                                break;
                            default:
                                break;
                        }
                    } catch (Exception e){
                        e.printStackTrace ();
                    }
                }
            }

        } catch (Exception e){
            e.printStackTrace ();
        }

        return super.dispatchKeyEvent (event);
    }


    @Override
    public void onScrollStateChanged (int state){
        super.onScrollStateChanged (state);
        switch (state){
            case RecyclerView.SCROLL_STATE_SETTLING:
                // TODO: 2020/4/23 滑动中
                mIsScrolling = true;
                break;
            case RecyclerView.SCROLL_STATE_IDLE:
                mIsScrolling = false;
                // TODO: 2020/4/23 滑动停止
                mScrollNumber = 0;
                break;
            default:
                break;
        }
    }

    public boolean isScrolling (){
        return getScrollState () != SCROLL_STATE_IDLE;
    }


        /**
     * 滚动到并选中某个位置
     *
     * @param position
     * @param isRequestFocus 是否选中获焦
     * @param isDelayed  是否需要延时(滑动距离过长的时候稍微加上延时能够更好的聚焦)
     */
    public void setScrollingSelectPosition (final int position , boolean isRequestFocus , boolean isDelayed){

        mSelectedPosition = position;

        // TODO: 2020/6/23  滚动到第几个栏目修改
        TvSmoothScroller scroller = new TvSmoothScroller (getContext () , mSelectedItemOffsetStart);
        scroller.setTargetPosition (position);
        getLayoutManager ().startSmoothScroll (scroller);

        if (isRequestFocus){
            postDelayed (new Runnable (){
                @Override
                public void run (){
                    try{
                        View view = getLayoutManager ().findViewByPosition (position);
                        view.requestFocus ();
                    } catch (Exception e){
                        e.printStackTrace ();
                    }
                }
            } , isDelayed ? 400 : 0);
        }
    }



    /**
     * 判断是否已经滑动到底部
     *
     * @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;
        }
    }

    /**
     * 是否是最右边的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;
    }

    @Override
    public void onFocusChange (View v , boolean hasFocus){
        LogUtils.e (hasFocus);
        //判断是否开启焦点记忆
        if (hasFocus){
            setDescendantFocusability (ViewGroup.FOCUS_AFTER_DESCENDANTS);
            if (mChildAdapterPosition > 0){
                View viewByPosition = getLayoutManager ().findViewByPosition (mChildAdapterPosition);
                if (viewByPosition != null){
                    viewByPosition.requestFocus ();
                }
            }
        } else{
            setDescendantFocusability (ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        }
    }

    private class TvSmoothScroller extends LinearSmoothScroller{
        private boolean mIsSmooth;
        private int mOffset;

        public TvSmoothScroller (Context context , int offset){
            super (context);
            mOffset = offset;
        }

        @Override
        protected void onTargetFound (View targetView , State state , Action action){
            if (null != getLayoutManager ()){
                getDecoratedBoundsWithMargins (targetView , mTempRect);
                mOffset = (getLayoutManager ().canScrollHorizontally () ? (getFreeWidth () - mTempRect.width ())
                        : (getFreeHeight () - mTempRect.height ())) / 2;
            }
            super.onTargetFound (targetView , state , action);
        }

        @Override
        public int calculateDtToFit (int viewStart , int viewEnd , int boxStart , int boxEnd , int snapPreference){
            int dt = boxStart - viewStart + mOffset;
            return dt;
        }

    }


如果遇到版本高一点的recycleview需要更改他的layoutmanager滑动方式

package com.hulian.newos.views;

import android.content.Context;
import android.graphics.Rect;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

import com.blankj.utilcode.util.LogUtils;


public class TVGridLayoutManager extends GridLayoutManager{
    private TvXRecyclerView mRecyclerView;

    public TVGridLayoutManager (TvXRecyclerView recyclerView , int spanCount){
        super (recyclerView.getContext () , spanCount);
        mRecyclerView = recyclerView;
    }

    @Override
    public void onLayoutChildren (RecyclerView.Recycler recycler , RecyclerView.State state){
        try{
            super.onLayoutChildren (recycler , state);
        } catch (IndexOutOfBoundsException e){
            e.printStackTrace ();
        }
    }

    /**
     * 滑动更改LinearLayoutManager一样改这里
     * @param parent
     * @param child
     * @param rect
     * @param immediate
     * @param focusedChildVisible
     * @return
     */
    @Override
    public boolean requestChildRectangleOnScreen (RecyclerView parent , View child , Rect rect , boolean immediate , boolean focusedChildVisible){
        return mRecyclerView.requestChildRectangleOnScreen (child , rect , immediate);
    }

    /**
     * 禁用预测动画。在RecyclerView中有一个bug,导致视图
     * 正在重新加载以从内部回收器堆栈中拉出无效的ViewHolders,如果
     * 自ViewHolder被回收以来,适配器大小减少了。
     */
    @Override
    public boolean supportsPredictiveItemAnimations (){
        return false;
    }
}

Logo

智屏生态联盟致力于大屏生态发展,利用大屏快应用技术降低开发者开发、发布大屏应用门槛

更多推荐