First on the implementation of the effect map

First, TV development in the custom RecyclerView to solve several problems

1, fast sliding focus running problem

Using native RecyclerView in Android TV terminal will find that the focus is missing in the process of fast sliding, which is very strange. Through reading relevant source code, IT is found that Focus analysis of Android TV development summary [focus] I summarized two kinds of solutions in looking up other big god, for your reference 1, rewrite the focusSearch method of RecyclerView

        @Override
    public View focusSearch(View focused, int direction) {
        View realNextFocus = super.focusSearch(focused, direction);
        View nextFocus = FocusFinder.getInstance().findNextFocus(this, focused, direction);
        switch (direction) {
            case FOCUS_RIGHT:
            case FOCUS_LEFT:
                // Invoke the removed listener
                if (nextFocus == null && !canScrollHorizontally(-1)) {
                    if (mCanFocusOutHorizontal) {
                        if(mFocusLostListener ! =null) {
                            mFocusLostListener.onFocusLost(focused, direction);
                        }
                        return realNextFocus;
                    } else {
                        returnfocused; }}break;
            case FOCUS_UP:
            case FOCUS_DOWN:
                if (nextFocus == null && !canScrollVertically(1)) {
                    if (mCanFocusOutVertical) {
                        return realNextFocus;
                    } else {
                        returnfocused; }}break;
        }
        return realNextFocus;
    }
Copy the code

2. Rewrite dispatchKeyEvent

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        boolean result = super.dispatchKeyEvent(event);
        View focusView = this.getFocusedChild();
        if (focusView == null) {
            return result;
        } 
        int dy = 0;
        int dx = 0;
        if (getChildCount() > 0) {
            View firstView = this.getChildAt(0);
            dy = firstView.getHeight();
            dx = firstView.getWidth();
        }
        if (event.getAction() == KeyEvent.ACTION_UP) {
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
               return super.dispatchKeyEvent(event);
            }
            return true;
         } else {
             switch (event.getKeyCode()) {
                 case KeyEvent.KEYCODE_DPAD_RIGHT:
                     View rightView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_RIGHT);
                     Log.i(TAG, "rightView is null:" + (rightView == null));
                     if(rightView ! =null) {
                         rightView.requestFocus();
                         return true;
                     } else {
                         this.smoothScrollBy(dx, 0);
                         return true;
                     }
                 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 {
                         this.smoothScrollBy(-dx, 0);
                         return true;
                     }
                 case KeyEvent.KEYCODE_DPAD_DOWN:
                     View downView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_DOWN);
                     Log.i(TAG, " downView is null:" + (downView == null));
                     if(downView ! =null) {
                         downView.requestFocus();
                         return true;
                     } else {
                         this.smoothScrollBy(0, dy);
                         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();
                         return true;
                     } else {
                         this.smoothScrollBy(0, -dy);
                         return true; }}}return result;
    }
Copy the code

2. Realize focus memory function

If you want to achieve focus memory function can rewrite the RecyclerView function as follows

private View mLastFocusView = null;
// Last focus position
private int mLastFocusPosition = 0;

@Override
public void requestChildFocus(View child, View focused) {
    Log.i(TAG, "requestChildFocus nextchild= " + child + ",focused = " + focused);
    Log.i(TAG, "requestChildFocus focusPos = " + mLastFocusPosition);
    super.requestChildFocus(child, focused);
    mLastFocusView = focused;
    // hasFocus becomes true after super.requestChildFocus is executed
    mLastFocusPosition = getChildViewHolder(child).getBindingAdapterPosition();
    Log.i(TAG, "requestChildFocus focusPos = " + mLastFocusPosition);
}


@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    Log.i(TAG, "addFocusables--focusPos = " + mLastFocusPosition);
    if (this.hasFocus() || mLastFocusView == null) {
        // Change the internal focus of recyclerView
        super.addFocusables(views, direction, focusableMode);
    } else {
        // Place the current view in the Focusable Views list, and the view will be retrieved when the focus is moved againviews.add(getLayoutManager().findViewByPosition(mLastFocusPosition)); }}Copy the code

3. The focus of Item is not blocked

If you want to enlarge the Item without blocking it, you need to override getChildDrawingOrder

@Override
protected int getChildDrawingOrder(int childCount, int position) {
    View focusedView = getFocusedChild();
    if (null! = focusedView) {int pos = indexOfChild(focusedView);
        /* This is the last item that needs to be refreshed
        if (position == childCount - 1) {
            if (pos > position) {
                pos = position;
            }
            return pos;
        }
        else if (pos == position) {
            /* This is the last item that was supposed to be refreshed */
            return childCount - 1; }}return position;
}
Copy the code

4. Realize the sliding effect of Item center

If you want to realize the center sliding effect, there are two ways of 1, rewrite RecyclerView requestChildFocus and requestChildRectangleOnScreen method

// Whether the focus is centered
private boolean mSelectedItemCentered = true;
private int mSelectedItemOffsetStart = 0;
private int mSelectedItemOffsetEnd = 0;

@Override
public void requestChildFocus(View child, View focused) {
   Log.i(TAG, "nextchild= " + child + ",focused = " + focused);
    // Calculate control recyclerView selected item center sub parameter
   if(mSelectedItemCentered && child ! =null) { mSelectedItemOffsetStart = ! isVertical() ? (getFreeWidth() - child.getWidth()) : (getFreeHeight() - child.getHeight()); mSelectedItemOffsetStart /=2;
       mSelectedItemOffsetEnd = mSelectedItemOffsetStart;
   }
   Log.i(TAG, "requestChildFocus focusPos = " + mCurrentFocusPosition);
   super.requestChildFocus(child, focused);
   // hasFocus becomes true after super.requestChildFocus is executed
   mCurrentFocusPosition = getChildViewHolder(child).getBindingAdapterPosition();
   Log.i(TAG, "requestChildFocus focusPos = " + mCurrentFocusPosition);
}

/** * This method centers the selected item * 

* this method determines the position between the child item and the parent when scrolling or sliding in the layout * dy, The actual meaning of dx is the distance between sliding in a scroll and sliding left and right * and this value can seriously affect the smoothness of the slide */

@Override public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { Log.i(TAG, "requestChildRectangleOnScreen= " + child + ",pos = " + 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(); // 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) { scrollBy(dx, dy); if (immediate) { scrollBy(dx, dy); } else { smoothScrollBy(dx, dy); } // Redraw to select item at the top. See getChildDrawingOrder for details postInvalidate(); return true; } return false; } Copy the code

2. Customize LayoutManager to achieve center sliding

public class CenterLayoutManager extends LinearLayoutManager {
    
    public CenterLayoutManager(Context context) {
        super(context);
    }

    @Override
    public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state,final int position) {
        CenterSmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext(),recyclerView) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                returncomputeVectorForPosition(targetPosition); }}; smoothScroller.setTargetPosition(position); startSmoothScroll(smoothScroller); }public PointF computeVectorForPosition(int targetPosition) {
        return super.computeScrollVectorForPosition(targetPosition);
    }

    abstract class CenterSmoothScroller extends LinearSmoothScroller {
        RecyclerView recyclerView;
        CenterSmoothScroller(Context context,RecyclerView recyclerView) {
            super(context);
            this.recyclerView = recyclerView;
        }

        @Override
        public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
            Log.i("du"."calculateDtToFit viewStart:" + viewStart + "---viewEnd:" + viewEnd + "---boxStart:" + boxStart + "---boxEnd:" + boxEnd + "----snapPreference:" + snapPreference );
            return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
        }

        /** * after the slide completes, let the item at the targetPosition get focus */
        @Override
        protected void onStop(a) {
            Log.i("du"."onStop-Position" + getTargetPosition());
            super.onStop();
            final View itemView = findViewByPosition(getTargetPosition());
            if (null! = itemView) { itemView.requestFocus(); }}}}/ / recyclerview calls
@Override
public void onBindViewHolder(final @NonNull BaseViewHolder viewHolder, final int i) {
    viewHolder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            Log.i("du"."onBindViewHolder-Position"+ i + "---hasFocus:" + hasFocus);
            if (hasFocus) {
                ViewCompat.animate(viewHolder.itemView).scaleX(1.5 f).scaleY(1.5 f).start();
                mCenterLayoutManager.smoothScrollToPosition(mRecyclerView,new RecyclerView.State(), i);
            } else {
                ViewCompat.animate(viewHolder.itemView).scaleX(1f).scaleY(1f).start(); }}});Copy the code

Second, realize the wireless circular sliding of vertical list

Here refer to the online method in recyclerView. Adapter method:

@Override
public int getItemCount(a) {
    return Integer.MAX_VALUE;
}
Copy the code

We need to adjust the logic to override the setAdapter since we need to center the wireless when we need to and default the positioning

@Override
public void setAdapter(@Nullable Adapter adapter) {
  super.setAdapter(adapter);
  // Locate to the corresponding position
  scrollToPosition(((Integer.MAX_VALUE / 2) - ((Integer.MAX_VALUE / 2) % realCount)) + mCurPos);
  postDelayed(new Runnable() {
      @Override
      public void run(a) {
          View targetView = getLayoutManager().findViewByPosition(((Integer.MAX_VALUE/2)-((Integer.MAX_VALUE/2)%realCount)) + mCurPos);
          if(targetView ! =null) { targetView.requestFocus(); }}},100);
}
Copy the code

Third, to achieve the effect of vertical list fading

Custom RecyclerView and rewrite the corresponding method

private Paint paint;
private int height;
private int width;
private int spanPixel = 100;

@Override
 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
     super.onSizeChanged(w, h, oldw, oldh);
     height = h;
     width = w;
     float spanFactor = spanPixel / (height / 2f);
     // Set fade to 0 0x00000000, spanFactor to 0xFF000000, and end to 0xFF000000
     LinearGradient linearGradient = new LinearGradient(0.0.0, height / 2.new int[] {0x00000000.0xff000000.0xff000000}, new float[] {0, spanFactor, 1f}, Shader.TileMode.MIRROR);
     paint.setShader(linearGradient);
 }


 @Override
 public void draw(Canvas c) {
     c.saveLayer(0.0, width, height, null, Canvas.ALL_SAVE_FLAG);
     super.draw(c);
     c.drawRect(0.0, width, height, paint);
     c.restore();
 }
Copy the code

Four, limit RecyclerView sliding speed

This can be used by adjusting the input interval of dispatchKeyEvent

private long mLastKeyDownTime;
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    long current = System.currentTimeMillis();
    if(event.getAction() ! = KeyEvent.ACTION_DOWN || getChildCount() ==0) {
        return super.dispatchKeyEvent(event);
    }
    // limit the minimum interval between two KEY_DOWN events to 120ms
    if (isComputingLayout() || current - mLastKeyDownTime <= 120) {
        return true;
    }
    mLastKeyDownTime = current;
    return super.dispatchKeyEvent(event);
}
Copy the code