Merged the latest changes

This commit is contained in:
Avently 2020-07-13 04:17:21 +03:00
commit d2aaa6f691
1254 changed files with 39193 additions and 18652 deletions

View file

@ -1,64 +1,66 @@
package org.schabi.newpipe.views;
import android.content.Context;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.ProgressBar;
import androidx.annotation.Nullable;
public final class AnimatedProgressBar extends ProgressBar {
@Nullable
private ProgressBarAnimation animation = null;
@Nullable
private ProgressBarAnimation animation = null;
public AnimatedProgressBar(final Context context) {
super(context);
}
public AnimatedProgressBar(Context context) {
super(context);
}
public AnimatedProgressBar(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public AnimatedProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AnimatedProgressBar(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public synchronized void setProgressAnimated(final int progress) {
cancelAnimation();
animation = new ProgressBarAnimation(this, getProgress(), progress);
startAnimation(animation);
}
public synchronized void setProgressAnimated(int progress) {
cancelAnimation();
animation = new ProgressBarAnimation(this, getProgress(), progress);
startAnimation(animation);
}
private void cancelAnimation() {
if (animation != null) {
animation.cancel();
animation = null;
}
clearAnimation();
}
private void cancelAnimation() {
if (animation != null) {
animation.cancel();
animation = null;
}
clearAnimation();
}
private static class ProgressBarAnimation extends Animation {
private static class ProgressBarAnimation extends Animation {
private final AnimatedProgressBar progressBar;
private final float from;
private final float to;
private final AnimatedProgressBar progressBar;
private final float from;
private final float to;
ProgressBarAnimation(final AnimatedProgressBar progressBar, final float from,
final float to) {
super();
this.progressBar = progressBar;
this.from = from;
this.to = to;
setDuration(500);
setInterpolator(new AccelerateDecelerateInterpolator());
}
ProgressBarAnimation(AnimatedProgressBar progressBar, float from, float to) {
super();
this.progressBar = progressBar;
this.from = from;
this.to = to;
setDuration(500);
setInterpolator(new AccelerateDecelerateInterpolator());
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
float value = from + (to - from) * interpolatedTime;
progressBar.setProgress((int) value);
}
}
@Override
protected void applyTransformation(final float interpolatedTime, final Transformation t) {
super.applyTransformation(interpolatedTime, t);
float value = from + (to - from) * interpolatedTime;
progressBar.setProgress((int) value);
}
}
}

View file

@ -23,13 +23,14 @@ import android.animation.ValueAnimator;
import android.content.Context;
import android.os.Build;
import android.os.Parcelable;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.LinearLayout;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.schabi.newpipe.util.AnimationUtils;
import java.lang.annotation.Retention;
@ -48,20 +49,36 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
public class CollapsibleView extends LinearLayout {
private static final String TAG = CollapsibleView.class.getSimpleName();
public CollapsibleView(Context context) {
private static final int ANIMATION_DURATION = 420;
public static final int COLLAPSED = 0;
public static final int EXPANDED = 1;
@State
@ViewMode
int currentState = COLLAPSED;
private boolean readyToChangeState;
private int targetHeight = -1;
private ValueAnimator currentAnimator;
private final List<StateListener> listeners = new ArrayList<>();
public CollapsibleView(final Context context) {
super(context);
}
public CollapsibleView(Context context, @Nullable AttributeSet attrs) {
public CollapsibleView(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
public CollapsibleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
public CollapsibleView(final Context context, @Nullable final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CollapsibleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
public CollapsibleView(final Context context, final AttributeSet attrs, final int defStyleAttr,
final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@ -69,20 +86,6 @@ public class CollapsibleView extends LinearLayout {
// Collapse/expand logic
//////////////////////////////////////////////////////////////////////////*/
private static final int ANIMATION_DURATION = 420;
public static final int COLLAPSED = 0, EXPANDED = 1;
@Retention(SOURCE)
@IntDef({COLLAPSED, EXPANDED})
public @interface ViewMode {}
@State @ViewMode int currentState = COLLAPSED;
private boolean readyToChangeState;
private int targetHeight = -1;
private ValueAnimator currentAnimator;
private final List<StateListener> listeners = new ArrayList<>();
/**
* This method recalculates the height of this view so it <b>must</b> be called when
* some child changes (e.g. add new views, change text).
@ -92,7 +95,8 @@ public class CollapsibleView extends LinearLayout {
Log.d(TAG, getDebugLogString("ready() called"));
}
measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), MeasureSpec.UNSPECIFIED);
measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
MeasureSpec.UNSPECIFIED);
targetHeight = getMeasuredHeight();
getLayoutParams().height = currentState == COLLAPSED ? 0 : targetHeight;
@ -111,7 +115,9 @@ public class CollapsibleView extends LinearLayout {
Log.d(TAG, getDebugLogString("collapse() called"));
}
if (!readyToChangeState) return;
if (!readyToChangeState) {
return;
}
final int height = getHeight();
if (height == 0) {
@ -119,7 +125,9 @@ public class CollapsibleView extends LinearLayout {
return;
}
if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel();
if (currentAnimator != null && currentAnimator.isRunning()) {
currentAnimator.cancel();
}
currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, 0);
setCurrentState(COLLAPSED);
@ -130,7 +138,9 @@ public class CollapsibleView extends LinearLayout {
Log.d(TAG, getDebugLogString("expand() called"));
}
if (!readyToChangeState) return;
if (!readyToChangeState) {
return;
}
final int height = getHeight();
if (height == this.targetHeight) {
@ -138,13 +148,17 @@ public class CollapsibleView extends LinearLayout {
return;
}
if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel();
if (currentAnimator != null && currentAnimator.isRunning()) {
currentAnimator.cancel();
}
currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, this.targetHeight);
setCurrentState(EXPANDED);
}
public void switchState() {
if (!readyToChangeState) return;
if (!readyToChangeState) {
return;
}
if (currentState == COLLAPSED) {
expand();
@ -158,7 +172,7 @@ public class CollapsibleView extends LinearLayout {
return currentState;
}
public void setCurrentState(@ViewMode int currentState) {
public void setCurrentState(@ViewMode final int currentState) {
this.currentState = currentState;
broadcastState();
}
@ -171,6 +185,7 @@ public class CollapsibleView extends LinearLayout {
/**
* Add a listener which will be listening for changes in this view (i.e. collapsed or expanded).
* @param listener {@link StateListener} to be added
*/
public void addListener(final StateListener listener) {
if (listeners.contains(listener)) {
@ -182,24 +197,12 @@ public class CollapsibleView extends LinearLayout {
/**
* Remove a listener so it doesn't receive more state changes.
* @param listener {@link StateListener} to be removed
*/
public void removeListener(final StateListener listener) {
listeners.remove(listener);
}
/**
* Simple interface used for listening state changes of the {@link CollapsibleView}.
*/
public interface StateListener {
/**
* Called when the state changes.
*
* @param newState the state that the {@link CollapsibleView} transitioned to,<br/>
* it's an integer being either {@link #COLLAPSED} or {@link #EXPANDED}
*/
void onStateChanged(@ViewMode int newState);
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@ -211,7 +214,7 @@ public class CollapsibleView extends LinearLayout {
}
@Override
public void onRestoreInstanceState(Parcelable state) {
public void onRestoreInstanceState(final Parcelable state) {
super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state));
ready();
@ -221,10 +224,29 @@ public class CollapsibleView extends LinearLayout {
// Internal
//////////////////////////////////////////////////////////////////////////*/
public String getDebugLogString(String description) {
public String getDebugLogString(final String description) {
return String.format("%-100s → %s",
description, "readyToChangeState = [" + readyToChangeState + "], currentState = [" + currentState + "], targetHeight = [" + targetHeight + "]," +
" mW x mH = [" + getMeasuredWidth() + "x" + getMeasuredHeight() + "]" +
" W x H = [" + getWidth() + "x" + getHeight() + "]");
description, "readyToChangeState = [" + readyToChangeState + "], "
+ "currentState = [" + currentState + "], "
+ "targetHeight = [" + targetHeight + "], "
+ "mW x mH = [" + getMeasuredWidth() + "x" + getMeasuredHeight() + "], "
+ "W x H = [" + getWidth() + "x" + getHeight() + "]");
}
@Retention(SOURCE)
@IntDef({COLLAPSED, EXPANDED})
public @interface ViewMode { }
/**
* Simple interface used for listening state changes of the {@link CollapsibleView}.
*/
public interface StateListener {
/**
* Called when the state changes.
*
* @param newState the state that the {@link CollapsibleView} transitioned to,<br/>
* it's an integer being either {@link #COLLAPSED} or {@link #EXPANDED}
*/
void onStateChanged(@ViewMode int newState);
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (C) Eltex ltd 2019 <eltex@eltex-co.ru>
* FocusAwareCoordinator.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;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
public final class FocusAwareCoordinator extends CoordinatorLayout {
private final Rect childFocus = new Rect();
public FocusAwareCoordinator(@NonNull final Context context) {
super(context);
}
public FocusAwareCoordinator(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
}
public FocusAwareCoordinator(@NonNull final Context context,
@Nullable final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void requestChildFocus(final View child, final View focused) {
super.requestChildFocus(child, focused);
if (!isInTouchMode()) {
if (focused.getHeight() >= getHeight()) {
focused.getFocusedRect(childFocus);
((ViewGroup) child).offsetDescendantRectToMyCoords(focused, childFocus);
} else {
focused.getHitRect(childFocus);
((ViewGroup) child).offsetDescendantRectToMyCoords((View) focused.getParent(),
childFocus);
}
requestChildRectangleOnScreen(child, childFocus, false);
}
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (C) Eltex ltd 2019 <eltex@eltex-co.ru>
* FocusAwareDrawerLayout.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.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.drawerlayout.widget.DrawerLayout;
import java.util.ArrayList;
public final class FocusAwareDrawerLayout extends DrawerLayout {
public FocusAwareDrawerLayout(@NonNull final Context context) {
super(context);
}
public FocusAwareDrawerLayout(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
}
public FocusAwareDrawerLayout(@NonNull final Context context,
@Nullable final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected boolean onRequestFocusInDescendants(final int direction,
final Rect previouslyFocusedRect) {
// SDK implementation of this method picks whatever visible View takes the focus first
// without regard to addFocusables. If the open drawer is temporarily empty, the focus
// escapes outside of it, which can be confusing
boolean hasOpenPanels = false;
for (int i = 0; i < getChildCount(); ++i) {
View child = getChildAt(i);
DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams();
if (lp.gravity != 0 && isDrawerVisible(child)) {
hasOpenPanels = true;
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
if (hasOpenPanels) {
return false;
}
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
@Override
public void addFocusables(final ArrayList<View> views, final int direction,
final int focusableMode) {
boolean hasOpenPanels = false;
View content = null;
for (int i = 0; i < getChildCount(); ++i) {
View child = getChildAt(i);
DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams();
if (lp.gravity == 0) {
content = child;
} else {
if (isDrawerVisible(child)) {
hasOpenPanels = true;
child.addFocusables(views, direction, focusableMode);
}
}
}
if (content != null && !hasOpenPanels) {
content.addFocusables(views, direction, focusableMode);
}
}
// this override isn't strictly necessary, but it is helpful when DrawerLayout isn't
// the topmost view in hierarchy (such as when system or builtin appcompat ActionBar is used)
@Override
@SuppressLint("RtlHardcoded")
public void openDrawer(@NonNull final View drawerView, final boolean animate) {
super.openDrawer(drawerView, animate);
drawerView.requestFocus(FOCUS_FORWARD);
}
}

View file

@ -0,0 +1,147 @@
/*
* Copyright (C) Eltex ltd 2019 <eltex@eltex-co.ru>
* FocusAwareDrawerLayout.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;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.ViewTreeObserver;
import android.widget.SeekBar;
import androidx.appcompat.widget.AppCompatSeekBar;
import org.schabi.newpipe.util.AndroidTvUtils;
/**
* SeekBar, adapted for directional navigation. It emulates touch-related callbacks
* (onStartTrackingTouch/onStopTrackingTouch), so existing code does not need to be changed to
* work with it.
*/
public final class FocusAwareSeekBar extends AppCompatSeekBar {
private NestedListener listener;
private ViewTreeObserver treeObserver;
public FocusAwareSeekBar(final Context context) {
super(context);
}
public FocusAwareSeekBar(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public FocusAwareSeekBar(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void setOnSeekBarChangeListener(final OnSeekBarChangeListener l) {
this.listener = l == null ? null : new NestedListener(l);
super.setOnSeekBarChangeListener(listener);
}
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
if (!isInTouchMode() && AndroidTvUtils.isConfirmKey(keyCode)) {
releaseTrack();
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onFocusChanged(final boolean gainFocus, final int direction,
final Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (!isInTouchMode() && !gainFocus) {
releaseTrack();
}
}
private final ViewTreeObserver.OnTouchModeChangeListener touchModeListener = isInTouchMode -> {
if (isInTouchMode) {
releaseTrack();
}
};
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
treeObserver = getViewTreeObserver();
treeObserver.addOnTouchModeChangeListener(touchModeListener);
}
@Override
protected void onDetachedFromWindow() {
if (treeObserver == null || !treeObserver.isAlive()) {
treeObserver = getViewTreeObserver();
}
treeObserver.removeOnTouchModeChangeListener(touchModeListener);
treeObserver = null;
super.onDetachedFromWindow();
}
private void releaseTrack() {
if (listener != null && listener.isSeeking) {
listener.onStopTrackingTouch(this);
}
}
private final class NestedListener implements OnSeekBarChangeListener {
private final OnSeekBarChangeListener delegate;
boolean isSeeking;
private NestedListener(final OnSeekBarChangeListener delegate) {
this.delegate = delegate;
}
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
if (!seekBar.isInTouchMode() && !isSeeking && fromUser) {
isSeeking = true;
onStartTrackingTouch(seekBar);
}
delegate.onProgressChanged(seekBar, progress, fromUser);
}
@Override
public void onStartTrackingTouch(final SeekBar seekBar) {
isSeeking = true;
delegate.onStartTrackingTouch(seekBar);
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
isSeeking = false;
delegate.onStopTrackingTouch(seekBar);
}
}
}

View file

@ -0,0 +1,293 @@
/*
* Copyright 2019 Alexander Rvachev <rvacheva@nxt.ru>
* FocusOverlayView.java is part of NewPipe
*
* License: GPL-3.0+
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.views;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.view.WindowCallbackWrapper;
import org.schabi.newpipe.R;
import java.lang.ref.WeakReference;
public final class FocusOverlayView extends Drawable implements
ViewTreeObserver.OnGlobalFocusChangeListener,
ViewTreeObserver.OnDrawListener,
ViewTreeObserver.OnGlobalLayoutListener,
ViewTreeObserver.OnScrollChangedListener, ViewTreeObserver.OnTouchModeChangeListener {
private boolean isInTouchMode;
private final Rect focusRect = new Rect();
private final Paint rectPaint = new Paint();
private final Handler animator = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(final Message msg) {
updateRect();
}
};
private WeakReference<View> focused;
public FocusOverlayView(final Context context) {
rectPaint.setStyle(Paint.Style.STROKE);
rectPaint.setStrokeWidth(2);
rectPaint.setColor(context.getResources().getColor(R.color.white));
}
@Override
public void onGlobalFocusChanged(final View oldFocus, final View newFocus) {
int l = focusRect.left;
int r = focusRect.right;
int t = focusRect.top;
int b = focusRect.bottom;
if (newFocus != null && newFocus.getWidth() > 0 && newFocus.getHeight() > 0) {
newFocus.getGlobalVisibleRect(focusRect);
focused = new WeakReference<>(newFocus);
} else {
focusRect.setEmpty();
focused = null;
}
if (l != focusRect.left || r != focusRect.right
|| t != focusRect.top || b != focusRect.bottom) {
invalidateSelf();
}
focused = new WeakReference<>(newFocus);
animator.sendEmptyMessageDelayed(0, 1000);
}
private void updateRect() {
if (focused == null) {
return;
}
View focusedView = this.focused.get();
int l = focusRect.left;
int r = focusRect.right;
int t = focusRect.top;
int b = focusRect.bottom;
if (focusedView != null) {
focusedView.getGlobalVisibleRect(focusRect);
} else {
focusRect.setEmpty();
}
if (l != focusRect.left || r != focusRect.right
|| t != focusRect.top || b != focusRect.bottom) {
invalidateSelf();
}
}
@Override
public void onDraw() {
updateRect();
}
@Override
public void onScrollChanged() {
updateRect();
animator.removeMessages(0);
animator.sendEmptyMessageDelayed(0, 1000);
}
@Override
public void onGlobalLayout() {
updateRect();
animator.sendEmptyMessageDelayed(0, 1000);
}
@Override
public void onTouchModeChanged(final boolean inTouchMode) {
this.isInTouchMode = inTouchMode;
if (inTouchMode) {
updateRect();
} else {
invalidateSelf();
}
}
public void setCurrentFocus(final View newFocus) {
if (newFocus == null) {
return;
}
this.isInTouchMode = newFocus.isInTouchMode();
onGlobalFocusChanged(null, newFocus);
}
@Override
public void draw(@NonNull final Canvas canvas) {
if (!isInTouchMode && focusRect.width() != 0) {
canvas.drawRect(focusRect, rectPaint);
}
}
@Override
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
@Override
public void setAlpha(final int alpha) {
}
@Override
public void setColorFilter(final ColorFilter colorFilter) {
}
public static void setupFocusObserver(final Dialog dialog) {
Rect displayRect = new Rect();
Window window = dialog.getWindow();
assert window != null;
View decor = window.getDecorView();
decor.getWindowVisibleDisplayFrame(displayRect);
FocusOverlayView overlay = new FocusOverlayView(dialog.getContext());
overlay.setBounds(0, 0, displayRect.width(), displayRect.height());
setupOverlay(window, overlay);
}
public static void setupFocusObserver(final Activity activity) {
Rect displayRect = new Rect();
Window window = activity.getWindow();
View decor = window.getDecorView();
decor.getWindowVisibleDisplayFrame(displayRect);
FocusOverlayView overlay = new FocusOverlayView(activity);
overlay.setBounds(0, 0, displayRect.width(), displayRect.height());
setupOverlay(window, overlay);
}
private static void setupOverlay(final Window window, final FocusOverlayView overlay) {
ViewGroup decor = (ViewGroup) window.getDecorView();
decor.getOverlay().add(overlay);
fixFocusHierarchy(decor);
ViewTreeObserver observer = decor.getViewTreeObserver();
observer.addOnScrollChangedListener(overlay);
observer.addOnGlobalFocusChangeListener(overlay);
observer.addOnGlobalLayoutListener(overlay);
observer.addOnTouchModeChangeListener(overlay);
overlay.setCurrentFocus(decor.getFocusedChild());
// Some key presses don't actually move focus, but still result in movement on screen.
// For example, MovementMethod of TextView may cause requestRectangleOnScreen() due to
// some "focusable" spans, which in turn causes CoordinatorLayout to "scroll" it's children.
// Unfortunately many such forms of "scrolling" do not count as scrolling for purpose
// of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly
// receiving keys from Window.
window.setCallback(new WindowCallbackWrapper(window.getCallback()) {
@Override
public boolean dispatchKeyEvent(final KeyEvent event) {
boolean res = super.dispatchKeyEvent(event);
overlay.onKey(event);
return res;
}
});
}
private void onKey(final KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return;
}
updateRect();
animator.sendEmptyMessageDelayed(0, 100);
}
private static void fixFocusHierarchy(final View decor) {
// During Android 8 development some dumb ass decided, that action bar has to be
// a keyboard focus cluster. Unfortunately, keyboard clusters do not work for primary
// auditory of key navigation Android TV users (Android TV remotes do not have
// keyboard META key for moving between clusters). We have to fix this unfortunate accident
// While we are at it, let's deal with touchscreenBlocksFocus too.
if (Build.VERSION.SDK_INT < 26) {
return;
}
if (!(decor instanceof ViewGroup)) {
return;
}
clearFocusObstacles((ViewGroup) decor);
}
@RequiresApi(api = 26)
private static void clearFocusObstacles(final ViewGroup viewGroup) {
viewGroup.setTouchscreenBlocksFocus(false);
if (viewGroup.isKeyboardNavigationCluster()) {
viewGroup.setKeyboardNavigationCluster(false);
return; // clusters aren't supposed to nest
}
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; ++i) {
View view = viewGroup.getChildAt(i);
if (view instanceof ViewGroup) {
clearFocusObstacles((ViewGroup) view);
}
}
}
}

View file

@ -0,0 +1,303 @@
/*
* Copyright 2019 Alexander Rvachev <rvacheva@nxt.ru>
* FocusOverlayView.java is part of NewPipe
*
* License: GPL-3.0+
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.views;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.TextView;
public class LargeTextMovementMethod extends LinkMovementMethod {
private final Rect visibleRect = new Rect();
private int direction;
@Override
public void onTakeFocus(final TextView view, final Spannable text, final int dir) {
Selection.removeSelection(text);
super.onTakeFocus(view, text, dir);
this.direction = dirToRelative(dir);
}
@Override
protected boolean handleMovementKey(final TextView widget,
final Spannable buffer,
final int keyCode,
final int movementMetaState,
final KeyEvent event) {
if (!doHandleMovement(widget, buffer, keyCode, movementMetaState, event)) {
// clear selection to make sure, that it does not confuse focus handling code
Selection.removeSelection(buffer);
return false;
}
return true;
}
private boolean doHandleMovement(final TextView widget,
final Spannable buffer,
final int keyCode,
final int movementMetaState,
final KeyEvent event) {
int newDir = keyToDir(keyCode);
if (direction != 0 && newDir != direction) {
return false;
}
this.direction = 0;
ViewGroup root = findScrollableParent(widget);
widget.getHitRect(visibleRect);
root.offsetDescendantRectToMyCoords((View) widget.getParent(), visibleRect);
return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
}
@Override
protected boolean up(final TextView widget, final Spannable buffer) {
if (gotoPrev(widget, buffer)) {
return true;
}
return super.up(widget, buffer);
}
@Override
protected boolean left(final TextView widget, final Spannable buffer) {
if (gotoPrev(widget, buffer)) {
return true;
}
return super.left(widget, buffer);
}
@Override
protected boolean right(final TextView widget, final Spannable buffer) {
if (gotoNext(widget, buffer)) {
return true;
}
return super.right(widget, buffer);
}
@Override
protected boolean down(final TextView widget, final Spannable buffer) {
if (gotoNext(widget, buffer)) {
return true;
}
return super.down(widget, buffer);
}
private boolean gotoPrev(final TextView view, final Spannable buffer) {
Layout layout = view.getLayout();
if (layout == null) {
return false;
}
View root = findScrollableParent(view);
int rootHeight = root.getHeight();
if (visibleRect.top >= 0) {
// we fit entirely into the viewport, no need for fancy footwork
return false;
}
int topExtra = -visibleRect.top;
int firstVisibleLineNumber = layout.getLineForVertical(topExtra);
// when deciding whether to pass "focus" to span, account for one more line
// this ensures, that focus is never passed to spans partially outside scroll window
int visibleStart = firstVisibleLineNumber == 0
? 0
: layout.getLineStart(firstVisibleLineNumber - 1);
ClickableSpan[] candidates = buffer.getSpans(
visibleStart, buffer.length(), ClickableSpan.class);
if (candidates.length != 0) {
int a = Selection.getSelectionStart(buffer);
int b = Selection.getSelectionEnd(buffer);
int selStart = Math.min(a, b);
int selEnd = Math.max(a, b);
int bestStart = -1;
int bestEnd = -1;
for (int i = 0; i < candidates.length; i++) {
int start = buffer.getSpanStart(candidates[i]);
int end = buffer.getSpanEnd(candidates[i]);
if ((end < selEnd || selStart == selEnd) && start >= visibleStart) {
if (end > bestEnd) {
bestStart = buffer.getSpanStart(candidates[i]);
bestEnd = end;
}
}
}
if (bestStart >= 0) {
Selection.setSelection(buffer, bestEnd, bestStart);
return true;
}
}
float fourLines = view.getTextSize() * 4;
visibleRect.left = 0;
visibleRect.right = view.getWidth();
visibleRect.top = Math.max(0, (int) (topExtra - fourLines));
visibleRect.bottom = visibleRect.top + rootHeight;
return view.requestRectangleOnScreen(visibleRect);
}
private boolean gotoNext(final TextView view, final Spannable buffer) {
Layout layout = view.getLayout();
if (layout == null) {
return false;
}
View root = findScrollableParent(view);
int rootHeight = root.getHeight();
if (visibleRect.bottom <= rootHeight) {
// we fit entirely into the viewport, no need for fancy footwork
return false;
}
int bottomExtra = visibleRect.bottom - rootHeight;
int visibleBottomBorder = view.getHeight() - bottomExtra;
int lineCount = layout.getLineCount();
int lastVisibleLineNumber = layout.getLineForVertical(visibleBottomBorder);
// when deciding whether to pass "focus" to span, account for one more line
// this ensures, that focus is never passed to spans partially outside scroll window
int visibleEnd = lastVisibleLineNumber == lineCount - 1
? buffer.length()
: layout.getLineEnd(lastVisibleLineNumber - 1);
ClickableSpan[] candidates = buffer.getSpans(0, visibleEnd, ClickableSpan.class);
if (candidates.length != 0) {
int a = Selection.getSelectionStart(buffer);
int b = Selection.getSelectionEnd(buffer);
int selStart = Math.min(a, b);
int selEnd = Math.max(a, b);
int bestStart = Integer.MAX_VALUE;
int bestEnd = Integer.MAX_VALUE;
for (int i = 0; i < candidates.length; i++) {
int start = buffer.getSpanStart(candidates[i]);
int end = buffer.getSpanEnd(candidates[i]);
if ((start > selStart || selStart == selEnd) && end <= visibleEnd) {
if (start < bestStart) {
bestStart = start;
bestEnd = buffer.getSpanEnd(candidates[i]);
}
}
}
if (bestEnd < Integer.MAX_VALUE) {
// cool, we have managed to find next link without having to adjust self within view
Selection.setSelection(buffer, bestStart, bestEnd);
return true;
}
}
// there are no links within visible area, but still some text past visible area
// scroll visible area further in required direction
float fourLines = view.getTextSize() * 4;
visibleRect.left = 0;
visibleRect.right = view.getWidth();
visibleRect.bottom = Math.min((int) (visibleBottomBorder + fourLines), view.getHeight());
visibleRect.top = visibleRect.bottom - rootHeight;
return view.requestRectangleOnScreen(visibleRect);
}
private ViewGroup findScrollableParent(final View view) {
View current = view;
ViewParent parent;
do {
parent = current.getParent();
if (parent == current || !(parent instanceof View)) {
return (ViewGroup) view.getRootView();
}
current = (View) parent;
if (current.isScrollContainer()) {
return (ViewGroup) current;
}
}
while (true);
}
private static int dirToRelative(final int dir) {
switch (dir) {
case View.FOCUS_DOWN:
case View.FOCUS_RIGHT:
return View.FOCUS_FORWARD;
case View.FOCUS_UP:
case View.FOCUS_LEFT:
return View.FOCUS_BACKWARD;
}
return dir;
}
private int keyToDir(final int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_LEFT:
return View.FOCUS_BACKWARD;
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_RIGHT:
return View.FOCUS_FORWARD;
}
return View.FOCUS_FORWARD;
}
}

View file

@ -0,0 +1,222 @@
/*
* 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;
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 Rect focusRect = new Rect();
private 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) {
View focusedItem = findContainingItemView(focused);
if (focusedItem != null) {
focusedItem.getHitRect(focusRect);
}
}
// call focusSearch() to initiate layout, but disregard returned View for now
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 >= 28) {
// 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;
}
FocusFinder finder = FocusFinder.getInstance();
// try to use FocusFinder instead of adapter
ViewGroup root = (ViewGroup) getRootView();
tempFocus.set(focusRect);
root.offsetDescendantRectToMyCoords(this, tempFocus);
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);
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;
}
}

View file

@ -0,0 +1,134 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import com.google.android.material.tabs.TabLayout;
/**
* A TabLayout that is scrollable when tabs exceed its width.
* Hides when there are less than 2 tabs.
*/
public class ScrollableTabLayout extends TabLayout {
private static final String TAG = ScrollableTabLayout.class.getSimpleName();
private int layoutWidth = 0;
private int prevVisibility = View.GONE;
public ScrollableTabLayout(final Context context) {
super(context);
}
public ScrollableTabLayout(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public ScrollableTabLayout(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r,
final int b) {
super.onLayout(changed, l, t, r, b);
remeasureTabs();
}
@Override
protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
layoutWidth = w;
}
@Override
public void addTab(@NonNull final Tab tab, final int position, final boolean setSelected) {
super.addTab(tab, position, setSelected);
hasMultipleTabs();
// Adding a tab won't decrease total tabs' width so tabMode won't have to change to FIXED
if (getTabMode() != MODE_SCROLLABLE) {
remeasureTabs();
}
}
@Override
public void removeTabAt(final int position) {
super.removeTabAt(position);
hasMultipleTabs();
// Removing a tab won't increase total tabs' width
// so tabMode won't have to change to SCROLLABLE
if (getTabMode() != MODE_FIXED) {
remeasureTabs();
}
}
@Override
protected void onVisibilityChanged(final View changedView, final int visibility) {
super.onVisibilityChanged(changedView, visibility);
// Check width if some tabs have been added/removed while ScrollableTabLayout was invisible
// We don't have to check if it was GONE because then requestLayout() will be called
if (changedView == this) {
if (prevVisibility == View.INVISIBLE) {
remeasureTabs();
}
prevVisibility = visibility;
}
}
private void setMode(final int mode) {
if (mode == getTabMode()) {
return;
}
setTabMode(mode);
}
/**
* Make ScrollableTabLayout not visible if there are less than two tabs.
*/
private void hasMultipleTabs() {
if (getTabCount() > 1) {
setVisibility(View.VISIBLE);
} else {
setVisibility(View.GONE);
}
}
/**
* Calculate minimal width required by tabs and set tabMode accordingly.
*/
private void remeasureTabs() {
if (prevVisibility != View.VISIBLE) {
return;
}
if (layoutWidth == 0) {
return;
}
final int count = getTabCount();
int contentWidth = 0;
for (int i = 0; i < count; i++) {
View child = getTabAt(i).view;
if (child.getVisibility() == View.VISIBLE) {
// Use tab's minimum requested width should actual content be too small
contentWidth += Math.max(child.getMinimumWidth(), child.getMeasuredWidth());
}
}
if (contentWidth > layoutWidth) {
setMode(TabLayout.MODE_SCROLLABLE);
} else {
setMode(TabLayout.MODE_FIXED);
}
}
}

View file

@ -0,0 +1,174 @@
/*
* Copyright (C) Eltex ltd 2019 <eltex@eltex-co.ru>
* SuperScrollLayoutManager.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;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
public final class SuperScrollLayoutManager extends LinearLayoutManager {
private final Rect handy = new Rect();
private final ArrayList<View> focusables = new ArrayList<>();
public SuperScrollLayoutManager(final Context context) {
super(context);
}
@Override
public boolean requestChildRectangleOnScreen(@NonNull final RecyclerView parent,
@NonNull final View child,
@NonNull final Rect rect,
final boolean immediate,
final boolean focusedChildVisible) {
if (!parent.isInTouchMode()) {
// only activate when in directional navigation mode (Android TV etc) fine grained
// touch scrolling is better served by nested scroll system
if (!focusedChildVisible || getFocusedChild() == child) {
handy.set(rect);
parent.offsetDescendantRectToMyCoords(child, handy);
parent.requestRectangleOnScreen(handy, immediate);
}
}
return super.requestChildRectangleOnScreen(parent, child, rect, immediate,
focusedChildVisible);
}
@Nullable
@Override
public View onInterceptFocusSearch(@NonNull final View focused, final int direction) {
View focusedItem = findContainingItemView(focused);
if (focusedItem == null) {
return super.onInterceptFocusSearch(focused, direction);
}
int listDirection = getAbsoluteDirection(direction);
if (listDirection == 0) {
return super.onInterceptFocusSearch(focused, direction);
}
// FocusFinder has an oddity: it considers size of Views more important
// than closeness to source View. This means, that big Views far away from current item
// are preferred to smaller sub-View of closer item. Setting focusability of closer item
// to FOCUS_AFTER_DESCENDANTS does not solve this, because ViewGroup#addFocusables omits
// such parent itself from list, if any of children are focusable.
// Fortunately we can intercept focus search and implement our own logic, based purely
// on position along the LinearLayoutManager axis
ViewGroup recycler = (ViewGroup) focusedItem.getParent();
int sourcePosition = getPosition(focusedItem);
if (sourcePosition == 0 && listDirection < 0) {
return super.onInterceptFocusSearch(focused, direction);
}
View preferred = null;
int distance = Integer.MAX_VALUE;
focusables.clear();
recycler.addFocusables(focusables, direction, recycler.isInTouchMode()
? View.FOCUSABLES_TOUCH_MODE
: View.FOCUSABLES_ALL);
try {
for (View view : focusables) {
if (view == focused || view == recycler) {
continue;
}
if (view == focusedItem) {
// do not pass focus back to the item View itself - it makes no sense
// (we can still pass focus to it's children however)
continue;
}
int candidate = getDistance(sourcePosition, view, listDirection);
if (candidate < 0) {
continue;
}
if (candidate < distance) {
distance = candidate;
preferred = view;
}
}
} finally {
focusables.clear();
}
return preferred;
}
private int getAbsoluteDirection(final int direction) {
switch (direction) {
default:
break;
case View.FOCUS_FORWARD:
return 1;
case View.FOCUS_BACKWARD:
return -1;
}
if (getOrientation() == RecyclerView.HORIZONTAL) {
switch (direction) {
default:
break;
case View.FOCUS_LEFT:
return getReverseLayout() ? 1 : -1;
case View.FOCUS_RIGHT:
return getReverseLayout() ? -1 : 1;
}
} else {
switch (direction) {
default:
break;
case View.FOCUS_UP:
return getReverseLayout() ? 1 : -1;
case View.FOCUS_DOWN:
return getReverseLayout() ? -1 : 1;
}
}
return 0;
}
private int getDistance(final int sourcePosition, final View candidate, final int direction) {
View itemView = findContainingItemView(candidate);
if (itemView == null) {
return -1;
}
int position = getPosition(itemView);
return direction * (position - sourcePosition);
}
}