mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-22 11:02:35 +01:00
Merge pull request #4833 from vkay94/youtube-rewind-forward
YouTube's Fast Forward/Rewind behavior
This commit is contained in:
commit
2886bc3b01
@ -75,6 +75,7 @@ fun View.animate(
|
|||||||
}
|
}
|
||||||
animate().setListener(null).cancel()
|
animate().setListener(null).cancel()
|
||||||
isVisible = true
|
isVisible = true
|
||||||
|
|
||||||
when (animationType) {
|
when (animationType) {
|
||||||
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
|
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||||
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||||
|
@ -51,9 +51,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|||||||
|
|
||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.animation.AnimatorListenerAdapter;
|
import android.animation.AnimatorListenerAdapter;
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.animation.PropertyValuesHolder;
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -154,6 +151,7 @@ import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
|||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
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.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerGestureListener;
|
import org.schabi.newpipe.player.event.PlayerGestureListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||||
@ -188,6 +186,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
|||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
||||||
|
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -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_DURATION = 300; // 300 millis
|
||||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
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 DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
|
||||||
|
public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Other constants
|
// Other constants
|
||||||
@ -313,7 +313,6 @@ public final class Player implements
|
|||||||
|
|
||||||
private PlayerBinding binding;
|
private PlayerBinding binding;
|
||||||
|
|
||||||
private ValueAnimator controlViewAnimator;
|
|
||||||
private final Handler controlsVisibilityHandler = new Handler();
|
private final Handler controlsVisibilityHandler = new Handler();
|
||||||
|
|
||||||
// fullscreen player
|
// fullscreen player
|
||||||
@ -365,6 +364,7 @@ public final class Player implements
|
|||||||
|
|
||||||
private int maxGestureLength; // scaled
|
private int maxGestureLength; // scaled
|
||||||
private GestureDetectorCompat gestureDetector;
|
private GestureDetectorCompat gestureDetector;
|
||||||
|
private PlayerGestureListener playerGestureListener;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Listeners and disposables
|
// Listeners and disposables
|
||||||
@ -449,6 +449,8 @@ public final class Player implements
|
|||||||
initPlayer(true);
|
initPlayer(true);
|
||||||
}
|
}
|
||||||
initListeners();
|
initListeners();
|
||||||
|
|
||||||
|
setupPlayerSeekOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initViews(@NonNull final PlayerBinding playerBinding) {
|
private void initViews(@NonNull final PlayerBinding playerBinding) {
|
||||||
@ -525,9 +527,9 @@ public final class Player implements
|
|||||||
binding.resizeTextView.setOnClickListener(this);
|
binding.resizeTextView.setOnClickListener(this);
|
||||||
binding.playbackLiveSync.setOnClickListener(this);
|
binding.playbackLiveSync.setOnClickListener(this);
|
||||||
|
|
||||||
final PlayerGestureListener listener = new PlayerGestureListener(this, service);
|
playerGestureListener = new PlayerGestureListener(this, service);
|
||||||
gestureDetector = new GestureDetectorCompat(context, listener);
|
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
|
||||||
binding.getRoot().setOnTouchListener(listener);
|
binding.getRoot().setOnTouchListener(playerGestureListener);
|
||||||
|
|
||||||
binding.queueButton.setOnClickListener(this);
|
binding.queueButton.setOnClickListener(this);
|
||||||
binding.segmentsButton.setOnClickListener(this);
|
binding.segmentsButton.setOnClickListener(this);
|
||||||
@ -578,6 +580,68 @@ public final class Player implements
|
|||||||
v.getPaddingRight(),
|
v.getPaddingRight(),
|
||||||
v.getPaddingBottom()));
|
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
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
@ -1796,71 +1860,6 @@ public final class Player implements
|
|||||||
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
|
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() {
|
public void showControlsThenHide() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "showControlsThenHide() called");
|
Log.d(TAG, "showControlsThenHide() called");
|
||||||
@ -1905,6 +1904,7 @@ public final class Player implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showHideShadow(final boolean show, final long duration) {
|
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.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||||
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
|
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||||
}
|
}
|
||||||
@ -2102,8 +2102,8 @@ public final class Player implements
|
|||||||
startProgressLoop();
|
startProgressLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
// if we are e.g. switching players, hide controls
|
||||||
animate(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION);
|
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||||
|
|
||||||
binding.playbackSeekBar.setEnabled(false);
|
binding.playbackSeekBar.setEnabled(false);
|
||||||
binding.playbackSeekBar.getThumb()
|
binding.playbackSeekBar.getThumb()
|
||||||
@ -2130,8 +2130,6 @@ public final class Player implements
|
|||||||
|
|
||||||
updateStreamRelatedViews();
|
updateStreamRelatedViews();
|
||||||
|
|
||||||
showAndAnimateControl(-1, true);
|
|
||||||
|
|
||||||
binding.playbackSeekBar.setEnabled(true);
|
binding.playbackSeekBar.setEnabled(true);
|
||||||
binding.playbackSeekBar.getThumb()
|
binding.playbackSeekBar.getThumb()
|
||||||
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
|
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
|
||||||
@ -2179,18 +2177,21 @@ public final class Player implements
|
|||||||
stopProgressLoop();
|
stopProgressLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
showControls(400);
|
// Don't let UI elements popup during double tap seeking. This state is entered sometimes
|
||||||
binding.loadingPanel.setVisibility(View.GONE);
|
// during seeking/loading. This if-else check ensures that the controls aren't popping up.
|
||||||
|
if (!playerGestureListener.isDoubleTapping()) {
|
||||||
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
showControls(400);
|
||||||
() -> {
|
binding.loadingPanel.setVisibility(View.GONE);
|
||||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
|
||||||
animatePlayButtons(true, 200);
|
|
||||||
if (!isQueueVisible) {
|
|
||||||
binding.playPauseButton.requestFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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);
|
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
|
||||||
|
|
||||||
// Remove running notification when user does not want minimization to background or popup
|
// Remove running notification when user does not want minimization to background or popup
|
||||||
@ -2208,7 +2209,6 @@ public final class Player implements
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onPausedSeek() called");
|
Log.d(TAG, "onPausedSeek() called");
|
||||||
}
|
}
|
||||||
showAndAnimateControl(-1, true);
|
|
||||||
|
|
||||||
animatePlayButtons(false, 100);
|
animatePlayButtons(false, 100);
|
||||||
binding.getRoot().setKeepScreenOn(true);
|
binding.getRoot().setKeepScreenOn(true);
|
||||||
@ -2838,7 +2838,6 @@ public final class Player implements
|
|||||||
}
|
}
|
||||||
seekBy(retrieveSeekDurationFromPreferences(this));
|
seekBy(retrieveSeekDurationFromPreferences(this));
|
||||||
triggerProgressUpdate();
|
triggerProgressUpdate();
|
||||||
showAndAnimateControl(R.drawable.ic_fast_forward, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fastRewind() {
|
public void fastRewind() {
|
||||||
@ -2847,7 +2846,6 @@ public final class Player implements
|
|||||||
}
|
}
|
||||||
seekBy(-retrieveSeekDurationFromPreferences(this));
|
seekBy(-retrieveSeekDurationFromPreferences(this));
|
||||||
triggerProgressUpdate();
|
triggerProgressUpdate();
|
||||||
showAndAnimateControl(R.drawable.ic_fast_rewind, true);
|
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
@ -4279,6 +4277,10 @@ public final class Player implements
|
|||||||
return binding.currentDisplaySeek;
|
return binding.currentDisplaySeek;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PlayerFastSeekOverlay getFastSeekOverlay() {
|
||||||
|
return binding.fastSeekOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public WindowManager.LayoutParams getPopupLayoutParams() {
|
public WindowManager.LayoutParams getPopupLayoutParams() {
|
||||||
return popupLayoutParams;
|
return popupLayoutParams;
|
||||||
|
@ -411,7 +411,7 @@ abstract class BasePlayerGestureListener(
|
|||||||
var doubleTapControls: DoubleTapListener? = null
|
var doubleTapControls: DoubleTapListener? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val isDoubleTapEnabled: Boolean
|
private val isDoubleTapEnabled: Boolean
|
||||||
get() = doubleTapDelay > 0
|
get() = doubleTapDelay > 0
|
||||||
|
|
||||||
var isDoubleTapping = false
|
var isDoubleTapping = false
|
||||||
@ -459,10 +459,6 @@ abstract class BasePlayerGestureListener(
|
|||||||
doubleTapControls?.onDoubleTapFinished()
|
doubleTapControls?.onDoubleTapFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableMultiDoubleTap(enable: Boolean) = apply {
|
|
||||||
doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
// ///////////////////////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////////////////////
|
||||||
|
@ -55,12 +55,10 @@ public class PlayerGestureListener
|
|||||||
player.hideControls(0, 0);
|
player.hideControls(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (portion == DisplayPortion.LEFT) {
|
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
|
||||||
player.fastRewind();
|
startMultiDoubleTap(event);
|
||||||
} else if (portion == DisplayPortion.MIDDLE) {
|
} else if (portion == DisplayPortion.MIDDLE) {
|
||||||
player.playPause();
|
player.playPause();
|
||||||
} else if (portion == DisplayPortion.RIGHT) {
|
|
||||||
player.fastForward();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,10 +230,10 @@ public class PlayerGestureListener
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onPopupResizingStart called");
|
Log.d(TAG, "onPopupResizingStart called");
|
||||||
}
|
}
|
||||||
player.showAndAnimateControl(-1, true);
|
|
||||||
player.getLoadingPanel().setVisibility(View.GONE);
|
player.getLoadingPanel().setVisibility(View.GONE);
|
||||||
|
|
||||||
player.hideControls(0, 0);
|
player.hideControls(0, 0);
|
||||||
|
animate(player.getFastSeekOverlay(), false, 0);
|
||||||
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
|
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
package org.schabi.newpipe.views.player
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, attrs) {
|
||||||
|
|
||||||
|
private var backgroundPaint = Paint()
|
||||||
|
|
||||||
|
private var widthPx = 0
|
||||||
|
private var heightPx = 0
|
||||||
|
|
||||||
|
// Background
|
||||||
|
|
||||||
|
private var shapePath = Path()
|
||||||
|
private var arcSize: Float = 80f
|
||||||
|
private var isLeft = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
requireNotNull(context) { "Context is null." }
|
||||||
|
|
||||||
|
backgroundPaint.apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
isAntiAlias = true
|
||||||
|
color = 0x30000000
|
||||||
|
}
|
||||||
|
|
||||||
|
val dm = context.resources.displayMetrics
|
||||||
|
widthPx = dm.widthPixels
|
||||||
|
heightPx = dm.heightPixels
|
||||||
|
|
||||||
|
updatePathShape()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateArcSize(baseView: View) {
|
||||||
|
val newArcSize = baseView.height / 11.4f
|
||||||
|
if (arcSize != newArcSize) {
|
||||||
|
arcSize = newArcSize
|
||||||
|
updatePathShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePosition(newIsLeft: Boolean) {
|
||||||
|
if (isLeft != newIsLeft) {
|
||||||
|
isLeft = newIsLeft
|
||||||
|
updatePathShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePathShape() {
|
||||||
|
val halfWidth = widthPx * 0.5f
|
||||||
|
|
||||||
|
shapePath.reset()
|
||||||
|
|
||||||
|
val w = if (isLeft) 0f else widthPx.toFloat()
|
||||||
|
val f = if (isLeft) 1 else -1
|
||||||
|
|
||||||
|
shapePath.moveTo(w, 0f)
|
||||||
|
shapePath.lineTo(f * (halfWidth - arcSize) + w, 0f)
|
||||||
|
shapePath.quadTo(
|
||||||
|
f * (halfWidth + arcSize) + w,
|
||||||
|
heightPx.toFloat() / 2,
|
||||||
|
f * (halfWidth - arcSize) + w,
|
||||||
|
heightPx.toFloat()
|
||||||
|
)
|
||||||
|
shapePath.lineTo(w, heightPx.toFloat())
|
||||||
|
|
||||||
|
shapePath.close()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
widthPx = w
|
||||||
|
heightPx = h
|
||||||
|
updatePathShape()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas?) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
canvas?.clipPath(shapePath)
|
||||||
|
canvas?.drawPath(shapePath, backgroundPaint)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
package org.schabi.newpipe.views.player
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.annotation.NonNull
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.END
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.player.event.DisplayPortion
|
||||||
|
import org.schabi.newpipe.player.event.DoubleTapListener
|
||||||
|
|
||||||
|
class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||||
|
ConstraintLayout(context, attrs), DoubleTapListener {
|
||||||
|
|
||||||
|
private var secondsView: SecondsView
|
||||||
|
private var circleClipTapView: CircleClipTapView
|
||||||
|
private var rootConstraintLayout: ConstraintLayout
|
||||||
|
|
||||||
|
private var wasForwarding: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
LayoutInflater.from(context).inflate(R.layout.player_fast_seek_overlay, this, true)
|
||||||
|
|
||||||
|
secondsView = findViewById(R.id.seconds_view)
|
||||||
|
circleClipTapView = findViewById(R.id.circle_clip_tap_view)
|
||||||
|
rootConstraintLayout = findViewById(R.id.root_constraint_layout)
|
||||||
|
|
||||||
|
addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
|
||||||
|
circleClipTapView.updateArcSize(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var performListener: PerformListener? = null
|
||||||
|
|
||||||
|
fun performListener(listener: PerformListener) = apply {
|
||||||
|
performListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
private var seekSecondsSupplier: () -> Int = { 0 }
|
||||||
|
|
||||||
|
fun seekSecondsSupplier(supplier: () -> Int) = apply {
|
||||||
|
seekSecondsSupplier = supplier
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicates whether this (double) tap is the first of a series
|
||||||
|
// Decides whether to call performListener.onAnimationStart or not
|
||||||
|
private var initTap: Boolean = false
|
||||||
|
|
||||||
|
override fun onDoubleTapStarted(portion: DisplayPortion) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "onDoubleTapStarted called with portion = [$portion]")
|
||||||
|
|
||||||
|
initTap = false
|
||||||
|
|
||||||
|
secondsView.stopAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDoubleTapProgressDown(portion: DisplayPortion) {
|
||||||
|
val shouldForward: Boolean =
|
||||||
|
performListener?.getFastSeekDirection(portion)?.directionAsBoolean ?: return
|
||||||
|
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onDoubleTapProgressDown called with " +
|
||||||
|
"shouldForward = [$shouldForward], " +
|
||||||
|
"wasForwarding = [$wasForwarding], " +
|
||||||
|
"initTap = [$initTap], "
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check if a initial tap occurred or if direction was switched
|
||||||
|
*/
|
||||||
|
if (!initTap || wasForwarding != shouldForward) {
|
||||||
|
// Reset seconds and update position
|
||||||
|
secondsView.seconds = 0
|
||||||
|
changeConstraints(shouldForward)
|
||||||
|
circleClipTapView.updatePosition(!shouldForward)
|
||||||
|
secondsView.setForwarding(shouldForward)
|
||||||
|
|
||||||
|
wasForwarding = shouldForward
|
||||||
|
|
||||||
|
if (!initTap) {
|
||||||
|
initTap = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
performListener?.onDoubleTap()
|
||||||
|
|
||||||
|
secondsView.seconds += seekSecondsSupplier.invoke()
|
||||||
|
performListener?.seek(forward = shouldForward)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDoubleTapFinished() {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]")
|
||||||
|
|
||||||
|
if (initTap) performListener?.onDoubleTapEnd()
|
||||||
|
initTap = false
|
||||||
|
|
||||||
|
secondsView.stopAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeConstraints(forward: Boolean) {
|
||||||
|
val constraintSet = ConstraintSet()
|
||||||
|
with(constraintSet) {
|
||||||
|
clone(rootConstraintLayout)
|
||||||
|
clear(secondsView.id, if (forward) START else END)
|
||||||
|
connect(
|
||||||
|
secondsView.id, if (forward) END else START,
|
||||||
|
PARENT_ID, if (forward) END else START
|
||||||
|
)
|
||||||
|
secondsView.startAnimation()
|
||||||
|
applyTo(rootConstraintLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerformListener {
|
||||||
|
fun onDoubleTap()
|
||||||
|
fun onDoubleTapEnd()
|
||||||
|
/**
|
||||||
|
* Determines if the playback should forward/rewind or do nothing.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
fun getFastSeekDirection(portion: DisplayPortion): FastSeekDirection
|
||||||
|
fun seek(forward: Boolean)
|
||||||
|
|
||||||
|
enum class FastSeekDirection(val directionAsBoolean: Boolean?) {
|
||||||
|
NONE(null),
|
||||||
|
FORWARD(true),
|
||||||
|
BACKWARD(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PlayerFastSeekOverlay"
|
||||||
|
private val DEBUG = MainActivity.DEBUG
|
||||||
|
}
|
||||||
|
}
|
181
app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt
Normal file
181
app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package org.schabi.newpipe.views.player
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.PlayerFastSeekSecondsViewBinding
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
|
|
||||||
|
class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ICON_ANIMATION_DURATION = 750L
|
||||||
|
}
|
||||||
|
|
||||||
|
var cycleDuration: Long = ICON_ANIMATION_DURATION
|
||||||
|
set(value) {
|
||||||
|
firstAnimator.duration = value / 5
|
||||||
|
secondAnimator.duration = value / 5
|
||||||
|
thirdAnimator.duration = value / 5
|
||||||
|
fourthAnimator.duration = value / 5
|
||||||
|
fifthAnimator.duration = value / 5
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds: Int = 0
|
||||||
|
set(value) {
|
||||||
|
binding.tvSeconds.text = context.resources.getQuantityString(
|
||||||
|
R.plurals.seconds, value, value
|
||||||
|
)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done as a field so that we don't have to compute on each tab if animations are enabled
|
||||||
|
private val animationsEnabled = DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)
|
||||||
|
|
||||||
|
val binding = PlayerFastSeekSecondsViewBinding.inflate(LayoutInflater.from(context), this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
orientation = VERTICAL
|
||||||
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setForwarding(isForward: Boolean) {
|
||||||
|
binding.triangleContainer.rotation = if (isForward) 0f else 180f
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startAnimation() {
|
||||||
|
stopAnimation()
|
||||||
|
|
||||||
|
if (animationsEnabled) {
|
||||||
|
firstAnimator.start()
|
||||||
|
} else {
|
||||||
|
// If no animations are enable show the arrow(s) without animation
|
||||||
|
showWithoutAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopAnimation() {
|
||||||
|
firstAnimator.cancel()
|
||||||
|
secondAnimator.cancel()
|
||||||
|
thirdAnimator.cancel()
|
||||||
|
fourthAnimator.cancel()
|
||||||
|
fifthAnimator.cancel()
|
||||||
|
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reset() {
|
||||||
|
binding.icon1.alpha = 0f
|
||||||
|
binding.icon2.alpha = 0f
|
||||||
|
binding.icon3.alpha = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showWithoutAnimation() {
|
||||||
|
binding.icon1.alpha = 1f
|
||||||
|
binding.icon2.alpha = 1f
|
||||||
|
binding.icon3.alpha = 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
private val firstAnimator: ValueAnimator = CustomValueAnimator(
|
||||||
|
{
|
||||||
|
binding.icon1.alpha = 0f
|
||||||
|
binding.icon2.alpha = 0f
|
||||||
|
binding.icon3.alpha = 0f
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binding.icon1.alpha = it
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secondAnimator.start()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private val secondAnimator: ValueAnimator = CustomValueAnimator(
|
||||||
|
{
|
||||||
|
binding.icon1.alpha = 1f
|
||||||
|
binding.icon2.alpha = 0f
|
||||||
|
binding.icon3.alpha = 0f
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binding.icon2.alpha = it
|
||||||
|
},
|
||||||
|
{
|
||||||
|
thirdAnimator.start()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private val thirdAnimator: ValueAnimator = CustomValueAnimator(
|
||||||
|
{
|
||||||
|
binding.icon1.alpha = 1f
|
||||||
|
binding.icon2.alpha = 1f
|
||||||
|
binding.icon3.alpha = 0f
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binding.icon1.alpha = 1f - binding.icon3.alpha
|
||||||
|
binding.icon3.alpha = it
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fourthAnimator.start()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private val fourthAnimator: ValueAnimator = CustomValueAnimator(
|
||||||
|
{
|
||||||
|
binding.icon1.alpha = 0f
|
||||||
|
binding.icon2.alpha = 1f
|
||||||
|
binding.icon3.alpha = 1f
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binding.icon2.alpha = 1f - it
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fifthAnimator.start()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private val fifthAnimator: ValueAnimator = CustomValueAnimator(
|
||||||
|
{
|
||||||
|
binding.icon1.alpha = 0f
|
||||||
|
binding.icon2.alpha = 0f
|
||||||
|
binding.icon3.alpha = 1f
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binding.icon3.alpha = 1f - it
|
||||||
|
},
|
||||||
|
{
|
||||||
|
firstAnimator.start()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private inner class CustomValueAnimator(
|
||||||
|
start: () -> Unit,
|
||||||
|
update: (value: Float) -> Unit,
|
||||||
|
end: () -> Unit
|
||||||
|
) : ValueAnimator() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
duration = cycleDuration / 5
|
||||||
|
setFloatValues(0f, 1f)
|
||||||
|
|
||||||
|
addUpdateListener { update(it.animatedValue as Float) }
|
||||||
|
addListener(object : AnimatorListener {
|
||||||
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
end()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator?) = Unit
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animator?) = Unit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/res/drawable/ic_play_seek_triangle.xml
Normal file
11
app/src/main/res/drawable/ic_play_seek_triangle.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M3,2 L22,12 L3,22 Z" />
|
||||||
|
|
||||||
|
</vector>
|
@ -54,11 +54,21 @@
|
|||||||
tools:ignore="ContentDescription"
|
tools:ignore="ContentDescription"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/playbackControlsShadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignBottom="@+id/playbackControlRoot"
|
||||||
|
android:background="@color/video_overlay_color"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<!-- transparent background is needed for selectableItemBackgroundBorderless to work -->
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/playbackControlRoot"
|
android:id="@+id/playbackControlRoot"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/video_overlay_color"
|
android:background="@color/transparent_background_color"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
@ -469,8 +479,8 @@
|
|||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_fullscreen"
|
app:srcCompat="@drawable/ic_fullscreen"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded"
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -493,8 +503,8 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_previous"
|
app:srcCompat="@drawable/ic_previous"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
|
||||||
@ -505,8 +515,8 @@
|
|||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_pause"
|
app:srcCompat="@drawable/ic_pause"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
@ -519,8 +529,8 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_next"
|
app:srcCompat="@drawable/ic_next"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -572,8 +582,8 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
app:srcCompat="@drawable/ic_close"
|
||||||
app:srcCompat="@drawable/ic_close" />
|
app:tint="@color/white" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/repeatButton"
|
android:id="@+id/repeatButton"
|
||||||
@ -637,24 +647,6 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/controlAnimationView"
|
|
||||||
android:layout_width="100dp"
|
|
||||||
android:layout_height="100dp"
|
|
||||||
android:background="@drawable/background_oval_black_transparent"
|
|
||||||
android:padding="15dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
tools:src="@drawable/ic_fast_rewind"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/loading_panel"
|
android:id="@+id/loading_panel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -754,4 +746,11 @@
|
|||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.player.PlayerFastSeekOverlay
|
||||||
|
android:id="@+id/fast_seek_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:alpha="0"
|
||||||
|
android:visibility="invisible" /> <!-- Required for the first appearance fading correctly -->
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
@ -54,11 +54,21 @@
|
|||||||
tools:ignore="ContentDescription"
|
tools:ignore="ContentDescription"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/playbackControlsShadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_alignBottom="@+id/playbackControlRoot"
|
||||||
|
android:background="@color/video_overlay_color"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<!-- transparent background is needed for selectableItemBackgroundBorderless to work -->
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/playbackControlRoot"
|
android:id="@+id/playbackControlRoot"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/video_overlay_color"
|
android:background="@color/transparent_background_color"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
@ -633,24 +643,6 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/controlAnimationView"
|
|
||||||
android:layout_width="100dp"
|
|
||||||
android:layout_height="100dp"
|
|
||||||
android:background="@drawable/background_oval_black_transparent"
|
|
||||||
android:padding="15dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
tools:src="@drawable/ic_fast_rewind"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/loading_panel"
|
android:id="@+id/loading_panel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -751,4 +743,11 @@
|
|||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.player.PlayerFastSeekOverlay
|
||||||
|
android:id="@+id/fast_seek_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:alpha="0"
|
||||||
|
android:visibility="invisible" /> <!-- Required for the first appearance fading correctly -->
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
27
app/src/main/res/layout/player_fast_seek_overlay.xml
Normal file
27
app/src/main/res/layout/player_fast_seek_overlay.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/root_constraint_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.player.CircleClipTapView
|
||||||
|
android:id="@+id/circle_clip_tap_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.player.SecondsView
|
||||||
|
android:id="@+id/seconds_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_default="percent"
|
||||||
|
app:layout_constraintWidth_percent="0.5" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
51
app/src/main/res/layout/player_fast_seek_seconds_view.xml
Normal file
51
app/src/main/res/layout/player_fast_seek_seconds_view.xml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:ignore="ContentDescription"
|
||||||
|
tools:layout_height="wrap_content"
|
||||||
|
tools:layout_width="match_parent"
|
||||||
|
tools:orientation="vertical"
|
||||||
|
tools:parentTag="android.widget.LinearLayout">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/triangle_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/icon_1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="@drawable/ic_play_seek_triangle"
|
||||||
|
tools:alpha="0.18" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/icon_2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="@drawable/ic_play_seek_triangle"
|
||||||
|
tools:alpha="0.5" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/icon_3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="@drawable/ic_play_seek_triangle"
|
||||||
|
tools:alpha="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_seconds"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="20 seconds" />
|
||||||
|
|
||||||
|
</merge>
|
Loading…
Reference in New Issue
Block a user