Skip to content

Commit

Permalink
Merge pull request #4833 from vkay94/youtube-rewind-forward
Browse files Browse the repository at this point in the history
YouTube's Fast Forward/Rewind behavior
  • Loading branch information
litetex authored Jan 30, 2022
2 parents 466db83 + af79479 commit 2886bc3
Show file tree
Hide file tree
Showing 12 changed files with 643 additions and 144 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/org/schabi/newpipe/ktx/View.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ fun View.animate(
}
animate().setListener(null).cancel()
isVisible = true

when (animationType) {
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
Expand Down
182 changes: 92 additions & 90 deletions app/src/main/java/org/schabi/newpipe/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
Expand Down Expand Up @@ -154,6 +151,7 @@
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.event.DisplayPortion;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.event.PlayerGestureListener;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
Expand Down Expand Up @@ -188,6 +186,7 @@
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView;
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;

import java.io.IOException;
import java.util.ArrayList;
Expand Down Expand Up @@ -247,6 +246,7 @@ public final class Player implements
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis

/*//////////////////////////////////////////////////////////////////////////
// Other constants
Expand Down Expand Up @@ -313,7 +313,6 @@ public final class Player implements

private PlayerBinding binding;

private ValueAnimator controlViewAnimator;
private final Handler controlsVisibilityHandler = new Handler();

// fullscreen player
Expand Down Expand Up @@ -365,6 +364,7 @@ public final class Player implements

private int maxGestureLength; // scaled
private GestureDetectorCompat gestureDetector;
private PlayerGestureListener playerGestureListener;

/*//////////////////////////////////////////////////////////////////////////
// Listeners and disposables
Expand Down Expand Up @@ -449,6 +449,8 @@ public void setupFromView(@NonNull final PlayerBinding playerBinding) {
initPlayer(true);
}
initListeners();

setupPlayerSeekOverlay();
}

private void initViews(@NonNull final PlayerBinding playerBinding) {
Expand Down Expand Up @@ -525,9 +527,9 @@ private void initListeners() {
binding.resizeTextView.setOnClickListener(this);
binding.playbackLiveSync.setOnClickListener(this);

final PlayerGestureListener listener = new PlayerGestureListener(this, service);
gestureDetector = new GestureDetectorCompat(context, listener);
binding.getRoot().setOnTouchListener(listener);
playerGestureListener = new PlayerGestureListener(this, service);
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
binding.getRoot().setOnTouchListener(playerGestureListener);

binding.queueButton.setOnClickListener(this);
binding.segmentsButton.setOnClickListener(this);
Expand Down Expand Up @@ -578,6 +580,68 @@ public void onChange(final boolean selfChange) {
v.getPaddingRight(),
v.getPaddingBottom()));
}

/**
* Initializes the Fast-For/Backward overlay.
*/
private void setupPlayerSeekOverlay() {
binding.fastSeekOverlay
.seekSecondsSupplier(
() -> (int) (retrieveSeekDurationFromPreferences(this) / 1000.0f))
.performListener(new PlayerFastSeekOverlay.PerformListener() {

@Override
public void onDoubleTap() {
animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION);
}

@Override
public void onDoubleTapEnd() {
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
}

@Override
public FastSeekDirection getFastSeekDirection(
@NonNull final DisplayPortion portion
) {
if (exoPlayerIsNull()) {
// Abort seeking
playerGestureListener.endMultiDoubleTap();
return FastSeekDirection.NONE;
}
if (portion == DisplayPortion.LEFT) {
// Check if it's possible to rewind
// Small puffer to eliminate infinite rewind seeking
if (simpleExoPlayer.getCurrentPosition() < 500L) {
return FastSeekDirection.NONE;
}
return FastSeekDirection.BACKWARD;
} else if (portion == DisplayPortion.RIGHT) {
// Check if it's possible to fast-forward
if (currentState == STATE_COMPLETED
|| simpleExoPlayer.getCurrentPosition()
>= simpleExoPlayer.getDuration()) {
return FastSeekDirection.NONE;
}
return FastSeekDirection.FORWARD;
}
/* portion == DisplayPortion.MIDDLE */
return FastSeekDirection.NONE;
}

@Override
public void seek(final boolean forward) {
playerGestureListener.keepInDoubleTapMode();
if (forward) {
fastForward();
} else {
fastRewind();
}
}
});
playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
}

//endregion


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

/**
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone.
*
* @param drawableId the drawable that will be used to animate,
* pass -1 to clear any animation that is visible
* @param goneOnEnd will set the animation view to GONE on the end of the animation
*/
public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) {
if (DEBUG) {
Log.d(TAG, "showAndAnimateControl() called with: "
+ "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
}
if (controlViewAnimator != null && controlViewAnimator.isRunning()) {
if (DEBUG) {
Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
}
controlViewAnimator.end();
}

if (drawableId == -1) {
if (binding.controlAnimationView.getVisibility() == View.VISIBLE) {
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
binding.controlAnimationView,
PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f),
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f)
).setDuration(DEFAULT_CONTROLS_DURATION);
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
binding.controlAnimationView.setVisibility(View.GONE);
}
});
controlViewAnimator.start();
}
return;
}

final float scaleFrom = goneOnEnd ? 1f : 1f;
final float scaleTo = goneOnEnd ? 1.8f : 1.4f;
final float alphaFrom = goneOnEnd ? 1f : 0f;
final float alphaTo = goneOnEnd ? 0f : 1f;


controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
binding.controlAnimationView,
PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo),
PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo),
PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo)
);
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE : View.VISIBLE);
}
});


binding.controlAnimationView.setVisibility(View.VISIBLE);
binding.controlAnimationView.setImageDrawable(
AppCompatResources.getDrawable(context, drawableId));
controlViewAnimator.start();
}

public void showControlsThenHide() {
if (DEBUG) {
Log.d(TAG, "showControlsThenHide() called");
Expand Down Expand Up @@ -1905,6 +1904,7 @@ public void hideControls(final long duration, final long delay) {
}

private void showHideShadow(final boolean show, final long duration) {
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
}
Expand Down Expand Up @@ -2102,8 +2102,8 @@ private void onBlocked() {
startProgressLoop();
}

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

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

updateStreamRelatedViews();

showAndAnimateControl(-1, true);

binding.playbackSeekBar.setEnabled(true);
binding.playbackSeekBar.getThumb()
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
Expand Down Expand Up @@ -2179,18 +2177,21 @@ private void onPaused() {
stopProgressLoop();
}

showControls(400);
binding.loadingPanel.setVisibility(View.GONE);

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

animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
() -> {
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
animatePlayButtons(true, 200);
if (!isQueueVisible) {
binding.playPauseButton.requestFocus();
}
});
}
changePopupWindowFlags(IDLE_WINDOW_FLAGS);

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

animatePlayButtons(false, 100);
binding.getRoot().setKeepScreenOn(true);
Expand Down Expand Up @@ -2838,7 +2838,6 @@ public void fastForward() {
}
seekBy(retrieveSeekDurationFromPreferences(this));
triggerProgressUpdate();
showAndAnimateControl(R.drawable.ic_fast_forward, true);
}

public void fastRewind() {
Expand All @@ -2847,7 +2846,6 @@ public void fastRewind() {
}
seekBy(-retrieveSeekDurationFromPreferences(this));
triggerProgressUpdate();
showAndAnimateControl(R.drawable.ic_fast_rewind, true);
}
//endregion

Expand Down Expand Up @@ -4279,6 +4277,10 @@ public TextView getCurrentDisplaySeek() {
return binding.currentDisplaySeek;
}

public PlayerFastSeekOverlay getFastSeekOverlay() {
return binding.fastSeekOverlay;
}

@Nullable
public WindowManager.LayoutParams getPopupLayoutParams() {
return popupLayoutParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ abstract class BasePlayerGestureListener(
var doubleTapControls: DoubleTapListener? = null
private set

val isDoubleTapEnabled: Boolean
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0

var isDoubleTapping = false
Expand Down Expand Up @@ -459,10 +459,6 @@ abstract class BasePlayerGestureListener(
doubleTapControls?.onDoubleTapFinished()
}

fun enableMultiDoubleTap(enable: Boolean) = apply {
doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0
}

// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,10 @@ public void onDoubleTap(@NonNull final MotionEvent event,
player.hideControls(0, 0);
}

if (portion == DisplayPortion.LEFT) {
player.fastRewind();
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
startMultiDoubleTap(event);
} else if (portion == DisplayPortion.MIDDLE) {
player.playPause();
} else if (portion == DisplayPortion.RIGHT) {
player.fastForward();
}
}

Expand Down Expand Up @@ -232,10 +230,10 @@ public void onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called");
}
player.showAndAnimateControl(-1, true);
player.getLoadingPanel().setVisibility(View.GONE);

player.hideControls(0, 0);
animate(player.getFastSeekOverlay(), false, 0);
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
}

Expand Down
Loading

0 comments on commit 2886bc3

Please sign in to comment.