Skip to content

Commit 2886bc3

Browse files
authored
Merge pull request #4833 from vkay94/youtube-rewind-forward
YouTube's Fast Forward/Rewind behavior
2 parents 466db83 + af79479 commit 2886bc3

12 files changed

Lines changed: 643 additions & 144 deletions

File tree

app/src/main/java/org/schabi/newpipe/ktx/View.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ fun View.animate(
7575
}
7676
animate().setListener(null).cancel()
7777
isVisible = true
78+
7879
when (animationType) {
7980
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
8081
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)

app/src/main/java/org/schabi/newpipe/player/Player.java

Lines changed: 92 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,6 @@
5151

5252
import android.animation.Animator;
5353
import android.animation.AnimatorListenerAdapter;
54-
import android.animation.ObjectAnimator;
55-
import android.animation.PropertyValuesHolder;
56-
import android.animation.ValueAnimator;
5754
import android.annotation.SuppressLint;
5855
import android.content.BroadcastReceiver;
5956
import android.content.Context;
@@ -154,6 +151,7 @@
154151
import org.schabi.newpipe.ktx.AnimationType;
155152
import org.schabi.newpipe.local.history.HistoryRecordManager;
156153
import org.schabi.newpipe.player.MainPlayer.PlayerType;
154+
import org.schabi.newpipe.player.event.DisplayPortion;
157155
import org.schabi.newpipe.player.event.PlayerEventListener;
158156
import org.schabi.newpipe.player.event.PlayerGestureListener;
159157
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
@@ -188,6 +186,7 @@
188186
import org.schabi.newpipe.util.external_communication.KoreUtils;
189187
import org.schabi.newpipe.util.external_communication.ShareUtils;
190188
import org.schabi.newpipe.views.ExpandableSurfaceView;
189+
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
191190

192191
import java.io.IOException;
193192
import java.util.ArrayList;
@@ -247,6 +246,7 @@ public final class Player implements
247246
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
248247
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
249248
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
249+
public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis
250250

251251
/*//////////////////////////////////////////////////////////////////////////
252252
// Other constants
@@ -313,7 +313,6 @@ public final class Player implements
313313

314314
private PlayerBinding binding;
315315

316-
private ValueAnimator controlViewAnimator;
317316
private final Handler controlsVisibilityHandler = new Handler();
318317

319318
// fullscreen player
@@ -365,6 +364,7 @@ public final class Player implements
365364

366365
private int maxGestureLength; // scaled
367366
private GestureDetectorCompat gestureDetector;
367+
private PlayerGestureListener playerGestureListener;
368368

369369
/*//////////////////////////////////////////////////////////////////////////
370370
// Listeners and disposables
@@ -449,6 +449,8 @@ public void setupFromView(@NonNull final PlayerBinding playerBinding) {
449449
initPlayer(true);
450450
}
451451
initListeners();
452+
453+
setupPlayerSeekOverlay();
452454
}
453455

454456
private void initViews(@NonNull final PlayerBinding playerBinding) {
@@ -525,9 +527,9 @@ private void initListeners() {
525527
binding.resizeTextView.setOnClickListener(this);
526528
binding.playbackLiveSync.setOnClickListener(this);
527529

528-
final PlayerGestureListener listener = new PlayerGestureListener(this, service);
529-
gestureDetector = new GestureDetectorCompat(context, listener);
530-
binding.getRoot().setOnTouchListener(listener);
530+
playerGestureListener = new PlayerGestureListener(this, service);
531+
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
532+
binding.getRoot().setOnTouchListener(playerGestureListener);
531533

532534
binding.queueButton.setOnClickListener(this);
533535
binding.segmentsButton.setOnClickListener(this);
@@ -578,6 +580,68 @@ public void onChange(final boolean selfChange) {
578580
v.getPaddingRight(),
579581
v.getPaddingBottom()));
580582
}
583+
584+
/**
585+
* Initializes the Fast-For/Backward overlay.
586+
*/
587+
private void setupPlayerSeekOverlay() {
588+
binding.fastSeekOverlay
589+
.seekSecondsSupplier(
590+
() -> (int) (retrieveSeekDurationFromPreferences(this) / 1000.0f))
591+
.performListener(new PlayerFastSeekOverlay.PerformListener() {
592+
593+
@Override
594+
public void onDoubleTap() {
595+
animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION);
596+
}
597+
598+
@Override
599+
public void onDoubleTapEnd() {
600+
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
601+
}
602+
603+
@Override
604+
public FastSeekDirection getFastSeekDirection(
605+
@NonNull final DisplayPortion portion
606+
) {
607+
if (exoPlayerIsNull()) {
608+
// Abort seeking
609+
playerGestureListener.endMultiDoubleTap();
610+
return FastSeekDirection.NONE;
611+
}
612+
if (portion == DisplayPortion.LEFT) {
613+
// Check if it's possible to rewind
614+
// Small puffer to eliminate infinite rewind seeking
615+
if (simpleExoPlayer.getCurrentPosition() < 500L) {
616+
return FastSeekDirection.NONE;
617+
}
618+
return FastSeekDirection.BACKWARD;
619+
} else if (portion == DisplayPortion.RIGHT) {
620+
// Check if it's possible to fast-forward
621+
if (currentState == STATE_COMPLETED
622+
|| simpleExoPlayer.getCurrentPosition()
623+
>= simpleExoPlayer.getDuration()) {
624+
return FastSeekDirection.NONE;
625+
}
626+
return FastSeekDirection.FORWARD;
627+
}
628+
/* portion == DisplayPortion.MIDDLE */
629+
return FastSeekDirection.NONE;
630+
}
631+
632+
@Override
633+
public void seek(final boolean forward) {
634+
playerGestureListener.keepInDoubleTapMode();
635+
if (forward) {
636+
fastForward();
637+
} else {
638+
fastRewind();
639+
}
640+
}
641+
});
642+
playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
643+
}
644+
581645
//endregion
582646

583647

@@ -1796,71 +1860,6 @@ public boolean isControlsVisible() {
17961860
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
17971861
}
17981862

1799-
/**
1800-
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone.
1801-
*
1802-
* @param drawableId the drawable that will be used to animate,
1803-
* pass -1 to clear any animation that is visible
1804-
* @param goneOnEnd will set the animation view to GONE on the end of the animation
1805-
*/
1806-
public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) {
1807-
if (DEBUG) {
1808-
Log.d(TAG, "showAndAnimateControl() called with: "
1809-
+ "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
1810-
}
1811-
if (controlViewAnimator != null && controlViewAnimator.isRunning()) {
1812-
if (DEBUG) {
1813-
Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
1814-
}
1815-
controlViewAnimator.end();
1816-
}
1817-
1818-
if (drawableId == -1) {
1819-
if (binding.controlAnimationView.getVisibility() == View.VISIBLE) {
1820-
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
1821-
binding.controlAnimationView,
1822-
PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f),
1823-
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f),
1824-
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f)
1825-
).setDuration(DEFAULT_CONTROLS_DURATION);
1826-
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
1827-
@Override
1828-
public void onAnimationEnd(final Animator animation) {
1829-
binding.controlAnimationView.setVisibility(View.GONE);
1830-
}
1831-
});
1832-
controlViewAnimator.start();
1833-
}
1834-
return;
1835-
}
1836-
1837-
final float scaleFrom = goneOnEnd ? 1f : 1f;
1838-
final float scaleTo = goneOnEnd ? 1.8f : 1.4f;
1839-
final float alphaFrom = goneOnEnd ? 1f : 0f;
1840-
final float alphaTo = goneOnEnd ? 0f : 1f;
1841-
1842-
1843-
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
1844-
binding.controlAnimationView,
1845-
PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo),
1846-
PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo),
1847-
PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo)
1848-
);
1849-
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
1850-
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
1851-
@Override
1852-
public void onAnimationEnd(final Animator animation) {
1853-
binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE : View.VISIBLE);
1854-
}
1855-
});
1856-
1857-
1858-
binding.controlAnimationView.setVisibility(View.VISIBLE);
1859-
binding.controlAnimationView.setImageDrawable(
1860-
AppCompatResources.getDrawable(context, drawableId));
1861-
controlViewAnimator.start();
1862-
}
1863-
18641863
public void showControlsThenHide() {
18651864
if (DEBUG) {
18661865
Log.d(TAG, "showControlsThenHide() called");
@@ -1905,6 +1904,7 @@ public void hideControls(final long duration, final long delay) {
19051904
}
19061905

19071906
private void showHideShadow(final boolean show, final long duration) {
1907+
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
19081908
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
19091909
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
19101910
}
@@ -2102,8 +2102,8 @@ private void onBlocked() {
21022102
startProgressLoop();
21032103
}
21042104

2105-
controlsVisibilityHandler.removeCallbacksAndMessages(null);
2106-
animate(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION);
2105+
// if we are e.g. switching players, hide controls
2106+
hideControls(DEFAULT_CONTROLS_DURATION, 0);
21072107

21082108
binding.playbackSeekBar.setEnabled(false);
21092109
binding.playbackSeekBar.getThumb()
@@ -2130,8 +2130,6 @@ private void onPlaying() {
21302130

21312131
updateStreamRelatedViews();
21322132

2133-
showAndAnimateControl(-1, true);
2134-
21352133
binding.playbackSeekBar.setEnabled(true);
21362134
binding.playbackSeekBar.getThumb()
21372135
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
@@ -2179,18 +2177,21 @@ private void onPaused() {
21792177
stopProgressLoop();
21802178
}
21812179

2182-
showControls(400);
2183-
binding.loadingPanel.setVisibility(View.GONE);
2184-
2185-
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
2186-
() -> {
2187-
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
2188-
animatePlayButtons(true, 200);
2189-
if (!isQueueVisible) {
2190-
binding.playPauseButton.requestFocus();
2191-
}
2192-
});
2180+
// Don't let UI elements popup during double tap seeking. This state is entered sometimes
2181+
// during seeking/loading. This if-else check ensures that the controls aren't popping up.
2182+
if (!playerGestureListener.isDoubleTapping()) {
2183+
showControls(400);
2184+
binding.loadingPanel.setVisibility(View.GONE);
21932185

2186+
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
2187+
() -> {
2188+
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
2189+
animatePlayButtons(true, 200);
2190+
if (!isQueueVisible) {
2191+
binding.playPauseButton.requestFocus();
2192+
}
2193+
});
2194+
}
21942195
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
21952196

21962197
// Remove running notification when user does not want minimization to background or popup
@@ -2208,7 +2209,6 @@ private void onPausedSeek() {
22082209
if (DEBUG) {
22092210
Log.d(TAG, "onPausedSeek() called");
22102211
}
2211-
showAndAnimateControl(-1, true);
22122212

22132213
animatePlayButtons(false, 100);
22142214
binding.getRoot().setKeepScreenOn(true);
@@ -2838,7 +2838,6 @@ public void fastForward() {
28382838
}
28392839
seekBy(retrieveSeekDurationFromPreferences(this));
28402840
triggerProgressUpdate();
2841-
showAndAnimateControl(R.drawable.ic_fast_forward, true);
28422841
}
28432842

28442843
public void fastRewind() {
@@ -2847,7 +2846,6 @@ public void fastRewind() {
28472846
}
28482847
seekBy(-retrieveSeekDurationFromPreferences(this));
28492848
triggerProgressUpdate();
2850-
showAndAnimateControl(R.drawable.ic_fast_rewind, true);
28512849
}
28522850
//endregion
28532851

@@ -4279,6 +4277,10 @@ public TextView getCurrentDisplaySeek() {
42794277
return binding.currentDisplaySeek;
42804278
}
42814279

4280+
public PlayerFastSeekOverlay getFastSeekOverlay() {
4281+
return binding.fastSeekOverlay;
4282+
}
4283+
42824284
@Nullable
42834285
public WindowManager.LayoutParams getPopupLayoutParams() {
42844286
return popupLayoutParams;

app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ abstract class BasePlayerGestureListener(
411411
var doubleTapControls: DoubleTapListener? = null
412412
private set
413413

414-
val isDoubleTapEnabled: Boolean
414+
private val isDoubleTapEnabled: Boolean
415415
get() = doubleTapDelay > 0
416416

417417
var isDoubleTapping = false
@@ -459,10 +459,6 @@ abstract class BasePlayerGestureListener(
459459
doubleTapControls?.onDoubleTapFinished()
460460
}
461461

462-
fun enableMultiDoubleTap(enable: Boolean) = apply {
463-
doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0
464-
}
465-
466462
// ///////////////////////////////////////////////////////////////////
467463
// Utils
468464
// ///////////////////////////////////////////////////////////////////

app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,10 @@ public void onDoubleTap(@NonNull final MotionEvent event,
5555
player.hideControls(0, 0);
5656
}
5757

58-
if (portion == DisplayPortion.LEFT) {
59-
player.fastRewind();
58+
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
59+
startMultiDoubleTap(event);
6060
} else if (portion == DisplayPortion.MIDDLE) {
6161
player.playPause();
62-
} else if (portion == DisplayPortion.RIGHT) {
63-
player.fastForward();
6462
}
6563
}
6664

@@ -232,10 +230,10 @@ public void onPopupResizingStart() {
232230
if (DEBUG) {
233231
Log.d(TAG, "onPopupResizingStart called");
234232
}
235-
player.showAndAnimateControl(-1, true);
236233
player.getLoadingPanel().setVisibility(View.GONE);
237234

238235
player.hideControls(0, 0);
236+
animate(player.getFastSeekOverlay(), false, 0);
239237
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
240238
}
241239

0 commit comments

Comments
 (0)