1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-26 13:02:34 +01:00

Merge pull request #4833 from vkay94/youtube-rewind-forward

YouTube's Fast Forward/Rewind behavior
This commit is contained in:
litetex 2022-01-30 17:07:15 +01:00 committed by GitHub
commit 2886bc3b01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 643 additions and 144 deletions

View File

@ -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)

View File

@ -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,6 +2177,9 @@ public final class Player implements
stopProgressLoop(); stopProgressLoop();
} }
// 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); showControls(400);
binding.loadingPanel.setVisibility(View.GONE); binding.loadingPanel.setVisibility(View.GONE);
@ -2190,7 +2191,7 @@ public final class Player implements
binding.playPauseButton.requestFocus(); 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;

View File

@ -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
// /////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////

View File

@ -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);
} }

View File

@ -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)
}
}

View File

@ -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
}
}

View 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
})
}
}
}

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>