/* * Copyright (C) Eltex ltd 2019 * NewPipeRecyclerView.java is part of NewPipe. * * NewPipe is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * NewPipe is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with NewPipe. If not, see . */ package org.schabi.newpipe.views; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.view.FocusFinder; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; public class NewPipeRecyclerView extends RecyclerView { private static final String TAG = "NewPipeRecyclerView"; private final Rect focusRect = new Rect(); private final Rect tempFocus = new Rect(); private boolean allowDpadScroll = true; public NewPipeRecyclerView(@NonNull final Context context) { super(context); init(); } public NewPipeRecyclerView(@NonNull final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); init(); } public NewPipeRecyclerView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); init(); } private void init() { setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); } public void setFocusScrollAllowed(final boolean allowed) { this.allowDpadScroll = allowed; } @Override public View focusSearch(final View focused, final int direction) { // RecyclerView has buggy focusSearch(), that calls into Adapter several times, // but ultimately fails to produce correct results in many cases. To add insult to injury, // it's focusSearch() hard-codes several behaviors, incompatible with widely accepted focus // handling practices: RecyclerView does not allow Adapter to give focus to itself (!!) and // always checks, that returned View is located in "correct" direction (which prevents us // from temporarily giving focus to special hidden View). return null; } @Override protected void removeDetachedView(final View child, final boolean animate) { if (child.hasFocus()) { // If the focused child is being removed (can happen during very fast scrolling), // temporarily give focus to ourselves. This will usually result in another child // gaining focus (which one does not really matter, because at that point scrolling // is FAST, and that child will soon be off-screen too) requestFocus(); } super.removeDetachedView(child, animate); } // we override focusSearch to always return null, so all moves moves lead to // dispatchUnhandledMove(). As added advantage, we can fully swallow some kinds of moves // (such as downward movement, that happens when loading additional contents is in progress @Override public boolean dispatchUnhandledMove(final View focused, final int direction) { tempFocus.setEmpty(); // save focus rect before further manipulation (both focusSearch() and scrollBy() // can mess with focused View by moving it off-screen and detaching) if (focused != null) { final View focusedItem = findContainingItemView(focused); if (focusedItem != null) { focusedItem.getHitRect(focusRect); } } // call focusSearch() to initiate layout, but disregard returned View for now final View adapterResult = super.focusSearch(focused, direction); if (adapterResult != null && !isOutside(adapterResult)) { adapterResult.requestFocus(direction); return true; } if (arrowScroll(direction)) { // if RecyclerView can not yield focus, but there is still some scrolling space in // indicated, direction, scroll some fixed amount in that direction // (the same logic in ScrollView) return true; } if (focused != this && direction == FOCUS_DOWN && !allowDpadScroll) { Log.i(TAG, "Consuming downward scroll: content load in progress"); return true; } if (tryFocusFinder(direction)) { return true; } if (adapterResult != null) { adapterResult.requestFocus(direction); return true; } return super.dispatchUnhandledMove(focused, direction); } private boolean tryFocusFinder(final int direction) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // Android 9 implemented bunch of handy changes to focus, that render code below less // useful, and also broke findNextFocusFromRect in way, that render this hack useless return false; } final FocusFinder finder = FocusFinder.getInstance(); // try to use FocusFinder instead of adapter final ViewGroup root = (ViewGroup) getRootView(); tempFocus.set(focusRect); root.offsetDescendantRectToMyCoords(this, tempFocus); final View focusFinderResult = finder.findNextFocusFromRect(root, tempFocus, direction); if (focusFinderResult != null && !isOutside(focusFinderResult)) { focusFinderResult.requestFocus(direction); return true; } // look for focus in our ancestors, increasing search scope with each failure // this provides much better locality than using FocusFinder with root ViewGroup parent = (ViewGroup) getParent(); while (parent != root) { tempFocus.set(focusRect); parent.offsetDescendantRectToMyCoords(this, tempFocus); final View candidate = finder.findNextFocusFromRect(parent, tempFocus, direction); if (candidate != null && candidate.requestFocus(direction)) { return true; } parent = (ViewGroup) parent.getParent(); } return false; } private boolean arrowScroll(final int direction) { switch (direction) { case FOCUS_DOWN: if (!canScrollVertically(1)) { return false; } scrollBy(0, 100); break; case FOCUS_UP: if (!canScrollVertically(-1)) { return false; } scrollBy(0, -100); break; case FOCUS_LEFT: if (!canScrollHorizontally(-1)) { return false; } scrollBy(-100, 0); break; case FOCUS_RIGHT: if (!canScrollHorizontally(-1)) { return false; } scrollBy(100, 0); break; default: return false; } return true; } private boolean isOutside(final View view) { return findContainingItemView(view) == null; } }