Xiaozou
Xiaozou

Reputation: 1665

How to keep the last item's focus of RecyclerView when navigating to the end of the list?

I used a RecyclerView with HORIZONTAL direction in my TV development which controlled by a D-pad to navigate the list from the left to right. the last item of the RecyclerView always lost focus when navigating to the right-most of the list.

So how can i keep the last item's focus when navigating to the end of the list?

Upvotes: 9

Views: 16381

Answers (4)

kirkadev
kirkadev

Reputation: 348

I'm adding an answer for Kotlin.

If in each place you will override the base layout, then your code can get a lot more complicated, especially since when working with TV you will want to add something behavior to layout manager.

I checked this on Xiaomi TV stick and X62 Max with D-PAD, also with emulators, it works.

So I suggest creating a class like this:

class TvLinearLayoutManager(
context: Context?,
orientation: Int,
reverseLayout: Boolean) : LinearLayoutManager(context, orientation, reverseLayout) {

override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
    return if (
    // This prevent focus jumping near border items
        (getPosition(focused)==itemCount-1 && direction == View.FOCUS_RIGHT) ||
        (getPosition(focused)==0 && direction == View.FOCUS_LEFT)
    )
        focused
    else
        super.onInterceptFocusSearch(focused, direction)
}}

Upvotes: 0

tmm1
tmm1

Reputation: 2115

If you're using a subclass of BaseGridView, like HorizontalGridView or VerticalGridView, set an onKeyInterceptListener that swallows the movement key at the end of the list. For example, with a HorizontalGridView:

grid.setOnKeyInterceptListener { event ->
    val focused = grid.focusedChild
    event?.keyCode == KEYCODE_DPAD_RIGHT && grid.layoutManager.getPosition(focused) == grid.adapter.itemCount-1
}

If you're using RecyclerView directly, then use onInterceptFocusSearch with a custom LinearLayoutManager. For example, with a LinearLayoutManager.VERTICAL list:

list.layoutManager = object: LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) {
    override fun onInterceptFocusSearch(focused: View?, direction: Int): View? {
        if (direction == View.FOCUS_DOWN) {
            val pos = getPosition(focused)
            if (pos == itemCount-1)
                return focused
        }
        if (direction == View.FOCUS_UP) {
            val pos = getPosition(focused)
            if (pos == 0)
                return focused
        }
        return super.onInterceptFocusSearch(focused, direction)
    }
}

Upvotes: 3

Edward Anderson
Edward Anderson

Reputation: 23

inspired bythis issues ,there is another workaround: in RecyclerView.Adapter<ViewHolder>

     int focusPos;

     @Override
        public void onBindViewHolder(ComposeViewHolder holder,
                final int position) {
            ....
            if (focusPos == position) { // focus last clicked view again
                holder.imageView.requestFocus();
            }
            ....
            holder.imageView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    ....
                    focusPos = position;
                    notifyDataSetChanged();
                }
            });
        }

Upvotes: 0

Xiaozou
Xiaozou

Reputation: 1665

I dug into the source code of RecyclerView, i found the onInterceptFocusSearch method in the LayoutManager, inner class of RecyclerView.

/**
 * This method gives a LayoutManager an opportunity to intercept the initial focus search
 * before the default behavior of {@link FocusFinder} is used. If this method returns
 * null FocusFinder will attempt to find a focusable child view. If it fails
 * then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)}
 * will be called to give the LayoutManager an opportunity to add new views for items
 * that did not have attached views representing them. The LayoutManager should not add
 * or remove views from this method.
 *
 * @param focused The currently focused view
 * @param direction One of { @link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
 *                  {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
 *                  {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
 * @return A descendant view to focus or null to fall back to default behavior.
 *         The default implementation returns null.
 */
public View onInterceptFocusSearch(View focused, int direction) {
    return null ;
}

which gives a LayoutManager an opportunity to intercept the initial focus search before the default behavior of FocusFinder is used.

So i overrided the onInterceptFocusSearch likes below, and used the CustomGridLayoutManager for my RecylerView, which works like a charming.

public class CustomGridLayoutManager extends android.support.v7.widget.GridLayoutManager {

        public CustomGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
                                 int defStyleRes) {
            super (context, attrs, defStyleAttr, defStyleRes);
        }

        public CustomGridLayoutManager(Context context, int spanCount) {
            super (context, spanCount);
        }

        public CustomGridLayoutManager(Context context, int spanCount, int orientation,
                                 boolean reverseLayout) {
            super (context, spanCount, orientation, reverseLayout);
        }

        @Override
        public View onInterceptFocusSearch(View focused, int direction) {
            int pos = getPosition(focused);
            int count = getItemCount();
            int orientation = getOrientation();


            **********
            do some logic
            what i did was return the focused View when the focused view is the last item of RecyclerView.
            **********

            return super .onInterceptFocusSearch(focused, direction);
        }
}

Upvotes: 16

Related Questions