2019-09-20 11:10:57 +02:00
|
|
|
/*
|
|
|
|
* Copyright (C) Eltex ltd 2019 <eltex@eltex-co.ru>
|
|
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
package org.schabi.newpipe.views;
|
|
|
|
|
|
|
|
import android.content.Context;
|
2019-11-08 08:26:12 +01:00
|
|
|
import android.graphics.Rect;
|
|
|
|
import android.os.Build;
|
2019-09-20 11:10:57 +02:00
|
|
|
import android.util.AttributeSet;
|
2019-11-08 08:26:12 +01:00
|
|
|
import android.util.Log;
|
|
|
|
import android.view.FocusFinder;
|
2019-09-20 11:10:57 +02:00
|
|
|
import android.view.View;
|
2019-11-08 08:26:12 +01:00
|
|
|
import android.view.ViewGroup;
|
2019-09-20 11:10:57 +02:00
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
|
|
|
|
public class NewPipeRecyclerView extends RecyclerView {
|
2019-11-08 08:26:12 +01:00
|
|
|
private static final String TAG = "NewPipeRecyclerView";
|
|
|
|
|
2020-11-18 23:50:00 +01:00
|
|
|
private final Rect focusRect = new Rect();
|
|
|
|
private final Rect tempFocus = new Rect();
|
2019-11-08 08:26:12 +01:00
|
|
|
|
2020-02-26 00:41:46 +01:00
|
|
|
private boolean allowDpadScroll = true;
|
2019-09-20 11:10:57 +02:00
|
|
|
|
2020-04-11 03:25:05 +02:00
|
|
|
public NewPipeRecyclerView(@NonNull final Context context) {
|
2019-09-20 11:10:57 +02:00
|
|
|
super(context);
|
2019-11-08 08:26:12 +01:00
|
|
|
|
|
|
|
init();
|
2019-09-20 11:10:57 +02:00
|
|
|
}
|
|
|
|
|
2020-04-11 04:03:22 +02:00
|
|
|
public NewPipeRecyclerView(@NonNull final Context context,
|
|
|
|
@Nullable final AttributeSet attrs) {
|
2019-09-20 11:10:57 +02:00
|
|
|
super(context, attrs);
|
2019-11-08 08:26:12 +01:00
|
|
|
|
|
|
|
init();
|
2019-09-20 11:10:57 +02:00
|
|
|
}
|
|
|
|
|
2020-04-11 04:03:22 +02:00
|
|
|
public NewPipeRecyclerView(@NonNull final Context context,
|
|
|
|
@Nullable final AttributeSet attrs, final int defStyle) {
|
2019-09-20 11:10:57 +02:00
|
|
|
super(context, attrs, defStyle);
|
2019-11-08 08:26:12 +01:00
|
|
|
|
|
|
|
init();
|
2019-09-20 11:10:57 +02:00
|
|
|
}
|
|
|
|
|
2019-11-08 08:26:12 +01:00
|
|
|
private void init() {
|
|
|
|
setFocusable(true);
|
|
|
|
|
|
|
|
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
|
|
|
|
}
|
|
|
|
|
2020-04-11 03:25:05 +02:00
|
|
|
public void setFocusScrollAllowed(final boolean allowed) {
|
|
|
|
this.allowDpadScroll = allowed;
|
2019-09-20 11:10:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2020-04-11 03:25:05 +02:00
|
|
|
public View focusSearch(final View focused, final int direction) {
|
2019-11-08 08:26:12 +01:00
|
|
|
// 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).
|
2019-09-20 11:10:57 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-11-08 08:26:12 +01:00
|
|
|
@Override
|
2020-04-11 03:25:05 +02:00
|
|
|
protected void removeDetachedView(final View child, final boolean animate) {
|
2019-11-08 08:26:12 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-04-11 04:03:22 +02:00
|
|
|
// 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
|
2019-11-08 08:26:12 +01:00
|
|
|
|
2019-09-20 11:10:57 +02:00
|
|
|
@Override
|
2020-04-11 03:25:05 +02:00
|
|
|
public boolean dispatchUnhandledMove(final View focused, final int direction) {
|
2019-11-08 08:26:12 +01:00
|
|
|
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) {
|
2020-08-16 10:24:58 +02:00
|
|
|
final View focusedItem = findContainingItemView(focused);
|
2019-11-08 08:26:12 +01:00
|
|
|
if (focusedItem != null) {
|
|
|
|
focusedItem.getHitRect(focusRect);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// call focusSearch() to initiate layout, but disregard returned View for now
|
2020-08-16 10:24:58 +02:00
|
|
|
final View adapterResult = super.focusSearch(focused, direction);
|
2019-11-08 08:26:12 +01:00
|
|
|
if (adapterResult != null && !isOutside(adapterResult)) {
|
|
|
|
adapterResult.requestFocus(direction);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (arrowScroll(direction)) {
|
2020-04-11 04:03:22 +02:00
|
|
|
// 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)
|
2019-09-20 11:10:57 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-11-08 08:26:12 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-04-11 03:25:05 +02:00
|
|
|
private boolean tryFocusFinder(final int direction) {
|
2020-08-27 22:59:29 +02:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
2020-04-11 04:03:22 +02:00
|
|
|
// 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
|
2019-11-08 08:26:12 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-08-16 10:24:58 +02:00
|
|
|
final FocusFinder finder = FocusFinder.getInstance();
|
2019-11-08 08:26:12 +01:00
|
|
|
|
|
|
|
// try to use FocusFinder instead of adapter
|
2020-08-16 10:24:58 +02:00
|
|
|
final ViewGroup root = (ViewGroup) getRootView();
|
2019-11-08 08:26:12 +01:00
|
|
|
|
|
|
|
tempFocus.set(focusRect);
|
|
|
|
|
|
|
|
root.offsetDescendantRectToMyCoords(this, tempFocus);
|
|
|
|
|
2020-08-16 10:24:58 +02:00
|
|
|
final View focusFinderResult = finder.findNextFocusFromRect(root, tempFocus, direction);
|
2019-11-08 08:26:12 +01:00
|
|
|
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);
|
|
|
|
|
2020-08-16 10:24:58 +02:00
|
|
|
final View candidate = finder.findNextFocusFromRect(parent, tempFocus, direction);
|
2019-11-08 08:26:12 +01:00
|
|
|
if (candidate != null && candidate.requestFocus(direction)) {
|
2019-09-20 11:10:57 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-11-08 08:26:12 +01:00
|
|
|
parent = (ViewGroup) parent.getParent();
|
2019-09-20 11:10:57 +02:00
|
|
|
}
|
|
|
|
|
2019-11-08 08:26:12 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-04-11 03:25:05 +02:00
|
|
|
private boolean arrowScroll(final int direction) {
|
2019-11-08 08:26:12 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-04-11 03:25:05 +02:00
|
|
|
private boolean isOutside(final View view) {
|
2019-11-08 08:26:12 +01:00
|
|
|
return findContainingItemView(view) == null;
|
2019-09-20 11:10:57 +02:00
|
|
|
}
|
|
|
|
}
|