From 3a40759cd2cc062424bb8709f40e9bfa8987bad2 Mon Sep 17 00:00:00 2001 From: vkay94 Date: Sat, 7 Nov 2020 15:29:07 +0100 Subject: [PATCH 01/21] SeekOverlay: Add Views --- .../newpipe/views/player/CircleClipTapView.kt | 106 +++++++ .../newpipe/views/player/PlayerSeekOverlay.kt | 264 ++++++++++++++++++ .../newpipe/views/player/SecondsView.kt | 171 ++++++++++++ .../res/drawable/ic_play_seek_triangle.xml | 11 + .../main/res/layout/player_seek_overlay.xml | 28 ++ .../res/layout/player_seek_seconds_view.xml | 50 ++++ 6 files changed, 630 insertions(+) create mode 100644 app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt create mode 100644 app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt create mode 100644 app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt create mode 100644 app/src/main/res/drawable/ic_play_seek_triangle.xml create mode 100644 app/src/main/res/layout/player_seek_overlay.xml create mode 100644 app/src/main/res/layout/player_seek_seconds_view.xml diff --git a/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt new file mode 100644 index 000000000..cbb4df738 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt @@ -0,0 +1,106 @@ +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 +import org.schabi.newpipe.player.event.DisplayPortion + +class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, attrs) { + + companion object { + const val COLOR_DARK = 0x40000000 + const val COLOR_DARK_TRANSPARENT = 0x30000000 + const val COLOR_LIGHT_TRANSPARENT = 0x25EEEEEE + + fun calculateArcSize(view: View): Float = view.height / 11.4f + } + + private var backgroundPaint = Paint() + + private var widthPx = 0 + private var heightPx = 0 + + // Background + + private var shapePath = Path() + private var isLeft = true + + init { + requireNotNull(context) { "Context is null." } + + backgroundPaint.apply { + style = Paint.Style.FILL + isAntiAlias = true + color = COLOR_LIGHT_TRANSPARENT + } + + val dm = context.resources.displayMetrics + widthPx = dm.widthPixels + heightPx = dm.heightPixels + + updatePathShape() + } + + var arcSize: Float = 80f + set(value) { + field = value + updatePathShape() + } + + var circleBackgroundColor: Int + get() = backgroundPaint.color + set(value) { + backgroundPaint.color = value + } + + /* + Background + */ + + fun updatePosition(portion: DisplayPortion) { + val newIsLeft = portion == DisplayPortion.LEFT + 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) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt new file mode 100644 index 000000000..5bdf0c97b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt @@ -0,0 +1,264 @@ +package org.schabi.newpipe.views.player + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import androidx.annotation.* +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.* +import androidx.core.content.ContextCompat +import androidx.core.widget.TextViewCompat +import androidx.preference.PreferenceManager +import kotlinx.android.synthetic.main.player_seek_overlay.view.* +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 PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) : + ConstraintLayout(context, attrs), DoubleTapListener { + + private var secondsView: SecondsView + private var circleClipTapView: CircleClipTapView + + private var isForwarding: Boolean? = null + + init { + LayoutInflater.from(context).inflate(R.layout.player_seek_overlay, this, true) + + secondsView = findViewById(R.id.seconds_view) + circleClipTapView = findViewById(R.id.circle_clip_tap_view) + + initializeAttributes() + secondsView.isForward = true + isForwarding = null + changeConstraints(true) + } + + private fun initializeAttributes() { + circleBackgroundColorInt(CircleClipTapView.COLOR_LIGHT_TRANSPARENT) + iconAnimationDuration(SecondsView.ICON_ANIMATION_DURATION) + icon(R.drawable.ic_play_seek_triangle) + + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val durationKey = context.getString(R.string.seek_duration_key) + val seekValue = prefs.getString( + durationKey, context.getString(R.string.seek_duration_default_value) + ) + seekSeconds(seekValue?.toInt()?.div(1000) ?: 10) + } + + private var performListener: PerformListener? = null + + fun performListener(listener: PerformListener) = apply { + performListener = listener + } + + var seekSeconds: Int = 0 + private set + + fun seekSeconds(seconds: Int) = apply { + seekSeconds = seconds + } + + var circleBackgroundColor: Int + get() = circleClipTapView.circleBackgroundColor + private set(value) { + circleClipTapView.circleBackgroundColor = value + } + + fun circleBackgroundColorRes(@ColorRes resId: Int) = apply { + circleBackgroundColor = ContextCompat.getColor(context, resId) + } + + fun circleBackgroundColorInt(@ColorInt color: Int) = apply { + circleBackgroundColor = color + } + + var arcSize: Float + get() = circleClipTapView.arcSize + internal set(value) { + circleClipTapView.arcSize = value + } + + fun arcSize(@DimenRes resId: Int) = apply { + arcSize = context.resources.getDimension(resId) + } + + fun arcSize(px: Float) = apply { + arcSize = px + } + + var showCircle: Boolean = true + private set(value) { + circleClipTapView.visibility = if (value) View.VISIBLE else View.GONE + field = value + } + + fun showCircle(show: Boolean) = apply { + showCircle = show + } + + var iconAnimationDuration: Long = SecondsView.ICON_ANIMATION_DURATION + get() = secondsView.cycleDuration + private set(value) { + secondsView.cycleDuration = value + field = value + } + + fun iconAnimationDuration(duration: Long) = apply { + iconAnimationDuration = duration + } + + @DrawableRes + var icon: Int = 0 + get() = secondsView.icon + private set(value) { + secondsView.stop() + secondsView.icon = value + field = value + } + + fun icon(@DrawableRes resId: Int) = apply { + icon = resId + } + + @StyleRes + var textAppearance: Int = 0 + private set(value) { + TextViewCompat.setTextAppearance(secondsView.textView, value) + field = value + } + + fun textAppearance(@StyleRes resId: Int) = apply { + textAppearance = resId + } + + // 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 + performListener?.onPrepare() + + changeConstraints(secondsView.isForward) + if (showCircle) circleClipTapView.updatePosition(portion) + + isForwarding = null + + if (this.alpha == 0f) + secondsView.stop() + } + + override fun onDoubleTapProgressDown(portion: DisplayPortion) { + val shouldForward: Boolean = performListener?.shouldFastForward(portion) ?: return + + if (DEBUG) + Log.d(TAG,"onDoubleTapProgressDown called with " + + "shouldForward = [$shouldForward], " + + "isForwarding = [$isForwarding], " + + "secondsView#isForward = [${secondsView.isForward}], " + + "initTap = [$initTap], ") + + // Using this check prevents from fast switching (one touches) + if (isForwarding != null && isForwarding != shouldForward) { + isForwarding = shouldForward + return + } + isForwarding = shouldForward + + if (this.visibility != View.VISIBLE || !initTap) { + if (!initTap) { + secondsView.seconds = 0 + performListener?.onAnimationStart() + secondsView.start() + initTap = true + } + } + + if (shouldForward) + forwarding() + else + rewinding() + } + + override fun onDoubleTapFinished() { + if (DEBUG) + Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]") + + if (initTap) performListener?.onAnimationEnd() + initTap = false + } + + private fun forwarding() { + if (DEBUG) + Log.d(TAG, "forwarding called") + + // First time tap or switched + if (!secondsView.isForward) { + changeConstraints(true) + if (showCircle) circleClipTapView.updatePosition(DisplayPortion.RIGHT) + secondsView.apply { + isForward = true + seconds = 0 + } + } + secondsView.seconds += seekSeconds + performListener?.seek(forward = true) + } + + private fun rewinding() { + if (DEBUG) + Log.d(TAG, "rewinding called") + + // First time tap or switched + if (secondsView.isForward) { + changeConstraints(false) + if (showCircle) circleClipTapView.updatePosition(DisplayPortion.LEFT) + secondsView.apply { + isForward = false + seconds = 0 + } + } + secondsView.seconds += seekSeconds + performListener?.seek(forward = false) + } + + private fun changeConstraints(forward: Boolean) { + val constraintSet = ConstraintSet() + with(constraintSet) { + clone(root_constraint_layout) + if (forward) { + clear(seconds_view.id, START) + connect(seconds_view.id, END, + PARENT_ID, END) + } else { + clear(seconds_view.id, END) + connect(seconds_view.id, START, + PARENT_ID, START) + } + secondsView.start() + applyTo(root_constraint_layout) + } + } + + interface PerformListener { + fun onPrepare() {} + fun onAnimationStart() + fun onAnimationEnd() + fun shouldFastForward(portion: DisplayPortion): Boolean? + fun seek(forward: Boolean) + } + + companion object { + private const val TAG = "PlayerSeekOverlay" + private val DEBUG = MainActivity.DEBUG + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt new file mode 100644 index 000000000..30bfe1217 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt @@ -0,0 +1,171 @@ +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.TextView +import androidx.annotation.DrawableRes +import androidx.constraintlayout.widget.ConstraintLayout +import kotlinx.android.synthetic.main.player_seek_seconds_view.view.* +import org.schabi.newpipe.R + +class SecondsView(context: Context?, attrs: AttributeSet?) : + ConstraintLayout(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) { + tv_seconds.text = context.resources.getQuantityString( + R.plurals.seconds, value, value + ) + field = value + } + + var isForward: Boolean = true + set(value) { + triangle_container.rotation = if (value) 0f else 180f + field = value + } + + val textView: TextView + get() = tv_seconds + + + @DrawableRes + var icon: Int = R.drawable.ic_play_seek_triangle + set(value) { + if (value > 0) { + icon_1.setImageResource(value) + icon_2.setImageResource(value) + icon_3.setImageResource(value) + } + field = value + } + + init { + LayoutInflater.from(context).inflate(R.layout.player_seek_seconds_view, this, true) + } + + fun start() { + stop() + firstAnimator.start() + } + + fun stop() { + firstAnimator.cancel() + secondAnimator.cancel() + thirdAnimator.cancel() + fourthAnimator.cancel() + fifthAnimator.cancel() + + reset() + } + + private fun reset() { + icon_1.alpha = 0f + icon_2.alpha = 0f + icon_3.alpha = 0f + } + + private val firstAnimator: ValueAnimator = CustomValueAnimator( + { + icon_1.alpha = 0f + icon_2.alpha = 0f + icon_3.alpha = 0f + }, { + icon_1.alpha = it + }, { + secondAnimator.start() + } + ) + + private val secondAnimator: ValueAnimator = CustomValueAnimator( + { + icon_1.alpha = 1f + icon_2.alpha = 0f + icon_3.alpha = 0f + }, { + icon_2.alpha = it + }, { + thirdAnimator.start() + } + ) + + private val thirdAnimator: ValueAnimator = CustomValueAnimator( + { + icon_1.alpha = 1f + icon_2.alpha = 1f + icon_3.alpha = 0f + }, { + icon_1.alpha = 1f - icon_3.alpha + icon_3.alpha = it + }, { + fourthAnimator.start() + } + ) + + private val fourthAnimator: ValueAnimator = CustomValueAnimator( + { + icon_1.alpha = 0f + icon_2.alpha = 1f + icon_3.alpha = 1f + }, { + icon_2.alpha = 1f - it + }, { + fifthAnimator.start() + } + ) + + private val fifthAnimator: ValueAnimator = CustomValueAnimator( + { + icon_1.alpha = 0f + icon_2.alpha = 0f + icon_3.alpha = 1f + }, { + icon_3.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 + + }) + } + } +} diff --git a/app/src/main/res/drawable/ic_play_seek_triangle.xml b/app/src/main/res/drawable/ic_play_seek_triangle.xml new file mode 100644 index 000000000..1aee026db --- /dev/null +++ b/app/src/main/res/drawable/ic_play_seek_triangle.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/player_seek_overlay.xml b/app/src/main/res/layout/player_seek_overlay.xml new file mode 100644 index 000000000..f4e9f1707 --- /dev/null +++ b/app/src/main/res/layout/player_seek_overlay.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_seek_seconds_view.xml b/app/src/main/res/layout/player_seek_seconds_view.xml new file mode 100644 index 000000000..14c9eaa2d --- /dev/null +++ b/app/src/main/res/layout/player_seek_seconds_view.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + From 72eb3b441596f93f7f1f14bdc7e98c296d4ef726 Mon Sep 17 00:00:00 2001 From: vkay94 Date: Mon, 18 Jan 2021 19:56:08 +0100 Subject: [PATCH 02/21] SeekOverlay: Add seek overlay logic to player --- .../main/java/org/schabi/newpipe/ktx/View.kt | 11 +- .../org/schabi/newpipe/player/Player.java | 121 +++++++++++++++--- .../player/event/PlayerGestureListener.java | 7 +- .../newpipe/views/player/CircleClipTapView.kt | 2 +- .../newpipe/views/player/PlayerSeekOverlay.kt | 48 ++++--- .../newpipe/views/player/SecondsView.kt | 40 +++--- app/src/main/res/layout-large-land/player.xml | 17 ++- app/src/main/res/layout/player.xml | 17 ++- 8 files changed, 206 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt index a1a96b20d..5adb4e3ef 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt @@ -54,7 +54,7 @@ fun View.animate( ) Log.d(TAG, "animate(): $msg") } - if (isVisible && enterOrExit) { + if (isVisible && enterOrExit && alpha == 1f && animationType == AnimationType.ALPHA) { if (MainActivity.DEBUG) { Log.d(TAG, "animate(): view was already visible > view = [$this]") } @@ -75,8 +75,15 @@ fun View.animate( } animate().setListener(null).cancel() isVisible = true + + val alphaRelativeDuration = if (enterOrExit && alpha < 1.0f) { + (duration * (1 - alpha)).toLong() + } else { + (duration * alpha).toLong() + } + when (animationType) { - AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd) + AnimationType.ALPHA -> animateAlpha(enterOrExit, alphaRelativeDuration, delay, execOnEnd) AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd) AnimationType.LIGHT_SCALE_AND_ALPHA -> animateLightScaleAndAlpha(enterOrExit, duration, delay, execOnEnd) AnimationType.SLIDE_AND_ALPHA -> animateSlideAndAlpha(enterOrExit, duration, delay, execOnEnd) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 81ef25db1..de8bffaf7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -136,6 +136,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; +import org.jetbrains.annotations.NotNull; import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; @@ -154,6 +155,7 @@ import org.schabi.newpipe.info_list.StreamSegmentAdapter; 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; @@ -188,6 +190,8 @@ import org.schabi.newpipe.util.StreamTypeUtil; 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.CircleClipTapView; +import org.schabi.newpipe.views.player.PlayerSeekOverlay; import java.io.IOException; import java.util.ArrayList; @@ -365,6 +369,7 @@ public final class Player implements private int maxGestureLength; // scaled private GestureDetectorCompat gestureDetector; + private PlayerGestureListener playerGestureListener; /*////////////////////////////////////////////////////////////////////////// // Listeners and disposables @@ -525,9 +530,10 @@ public final class Player implements 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); + setupPlayerSeekOverlay(); binding.queueButton.setOnClickListener(this); binding.segmentsButton.setOnClickListener(this); @@ -578,6 +584,83 @@ public final class Player implements v.getPaddingRight(), v.getPaddingBottom())); } + + private void setupPlayerSeekOverlay() { + final int fadeDurations = 450; + binding.seekOverlay.showCircle(true) + .circleBackgroundColorInt(CircleClipTapView.COLOR_DARK_TRANSPARENT) + .seekSeconds(retrieveSeekDurationFromPreferences(this) / 1000) + .performListener(new PlayerSeekOverlay.PerformListener() { + + @Override + public void onPrepare() { + if (invalidSeekConditions()) { + playerGestureListener.endMultiDoubleTap(); + return; + } + binding.seekOverlay.arcSize( + CircleClipTapView.Companion.calculateArcSize(getSurfaceView()) + ); + } + + @Override + public void onAnimationStart() { + animate(binding.seekOverlay, true, fadeDurations); + animate(binding.playbackControlsShadow, + !simpleExoPlayer.getPlayWhenReady(), fadeDurations); + animate(binding.playerTopShadow, false, fadeDurations); + animate(binding.playerBottomShadow, false, fadeDurations); + animate(binding.playbackControlRoot, false, fadeDurations); + hideSystemUIIfNeeded(); + } + + @Override + public void onAnimationEnd() { + animate(binding.seekOverlay, false, fadeDurations); + if (!simpleExoPlayer.getPlayWhenReady()) { + showControls(fadeDurations); + } else { + showHideShadow(false, fadeDurations); + } + } + + @Override + public Boolean shouldFastForward(@NotNull final DisplayPortion portion) { + // Null indicates an invalid area or condition e.g. the middle portion + // or video start or end was reached during double tap seeking + if (invalidSeekConditions()) { + return null; + } + if (portion == DisplayPortion.LEFT + // Small puffer to eliminate infinite rewind seeking + && simpleExoPlayer.getCurrentPosition() > 500L) { + return false; + } else if (portion == DisplayPortion.RIGHT) { + return true; + } else /* portion == DisplayPortion.MIDDLE */ { + return null; + } + } + + @Override + public void seek(final boolean forward) { + playerGestureListener.keepInDoubleTapMode(); + if (forward) { + fastForward(); + } else { + fastRewind(); + } + } + + private boolean invalidSeekConditions() { + return simpleExoPlayer.getCurrentPosition() == simpleExoPlayer.getDuration() + || currentState == STATE_COMPLETED + || !isPrepared; + } + }); + playerGestureListener.doubleTapControls(binding.seekOverlay); + } + //endregion @@ -1905,6 +1988,7 @@ public final class Player implements } 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); } @@ -2179,18 +2263,21 @@ public final class Player implements 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 @@ -2838,7 +2925,6 @@ public final class Player implements } seekBy(retrieveSeekDurationFromPreferences(this)); triggerProgressUpdate(); - showAndAnimateControl(R.drawable.ic_fast_forward, true); } public void fastRewind() { @@ -2847,7 +2933,6 @@ public final class Player implements } seekBy(-retrieveSeekDurationFromPreferences(this)); triggerProgressUpdate(); - showAndAnimateControl(R.drawable.ic_fast_rewind, true); } //endregion @@ -4279,6 +4364,10 @@ public final class Player implements return binding.currentDisplaySeek; } + public PlayerSeekOverlay getSeekOverlay() { + return binding.seekOverlay; + } + @Nullable public WindowManager.LayoutParams getPopupLayoutParams() { return popupLayoutParams; diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index 25ace1c05..b215584e8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -55,12 +55,10 @@ public class PlayerGestureListener 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(); } } @@ -236,6 +234,7 @@ public class PlayerGestureListener player.getLoadingPanel().setVisibility(View.GONE); player.hideControls(0, 0); + animate(player.getSeekOverlay(), false, 0); animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0); } diff --git a/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt index cbb4df738..6b22538e0 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt @@ -11,7 +11,7 @@ import org.schabi.newpipe.player.event.DisplayPortion class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, attrs) { companion object { - const val COLOR_DARK = 0x40000000 + const val COLOR_DARK = 0x45000000 const val COLOR_DARK_TRANSPARENT = 0x30000000 const val COLOR_LIGHT_TRANSPARENT = 0x25EEEEEE diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt index 5bdf0c97b..d61989d92 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt @@ -5,24 +5,30 @@ import android.util.AttributeSet import android.util.Log import android.view.LayoutInflater import android.view.View -import androidx.annotation.* +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DimenRes +import androidx.annotation.DrawableRes +import androidx.annotation.StyleRes 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 androidx.constraintlayout.widget.ConstraintSet.* import androidx.core.content.ContextCompat import androidx.core.widget.TextViewCompat import androidx.preference.PreferenceManager -import kotlinx.android.synthetic.main.player_seek_overlay.view.* 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 PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) : +class PlayerSeekOverlay(context: Context, private val attrs: AttributeSet?) : ConstraintLayout(context, attrs), DoubleTapListener { private var secondsView: SecondsView private var circleClipTapView: CircleClipTapView + private var rootConstraintLayout: ConstraintLayout private var isForwarding: Boolean? = null @@ -31,6 +37,7 @@ class PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) : secondsView = findViewById(R.id.seconds_view) circleClipTapView = findViewById(R.id.circle_clip_tap_view) + rootConstraintLayout = findViewById(R.id.root_constraint_layout) initializeAttributes() secondsView.isForward = true @@ -161,11 +168,14 @@ class PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) : val shouldForward: Boolean = performListener?.shouldFastForward(portion) ?: return if (DEBUG) - Log.d(TAG,"onDoubleTapProgressDown called with " + - "shouldForward = [$shouldForward], " + - "isForwarding = [$isForwarding], " + - "secondsView#isForward = [${secondsView.isForward}], " + - "initTap = [$initTap], ") + Log.d( + TAG, + "onDoubleTapProgressDown called with " + + "shouldForward = [$shouldForward], " + + "isForwarding = [$isForwarding], " + + "secondsView#isForward = [${secondsView.isForward}], " + + "initTap = [$initTap], " + ) // Using this check prevents from fast switching (one touches) if (isForwarding != null && isForwarding != shouldForward) { @@ -234,18 +244,22 @@ class PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) : private fun changeConstraints(forward: Boolean) { val constraintSet = ConstraintSet() with(constraintSet) { - clone(root_constraint_layout) + clone(rootConstraintLayout) if (forward) { - clear(seconds_view.id, START) - connect(seconds_view.id, END, - PARENT_ID, END) + clear(secondsView.id, START) + connect( + secondsView.id, END, + PARENT_ID, END + ) } else { - clear(seconds_view.id, END) - connect(seconds_view.id, START, - PARENT_ID, START) + clear(secondsView.id, END) + connect( + secondsView.id, START, + PARENT_ID, START + ) } secondsView.start() - applyTo(root_constraint_layout) + applyTo(rootConstraintLayout) } } diff --git a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt index 30bfe1217..fa2dbd63e 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt @@ -11,7 +11,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import kotlinx.android.synthetic.main.player_seek_seconds_view.view.* import org.schabi.newpipe.R -class SecondsView(context: Context?, attrs: AttributeSet?) : +class SecondsView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) { companion object { @@ -45,7 +45,6 @@ class SecondsView(context: Context?, attrs: AttributeSet?) : val textView: TextView get() = tv_seconds - @DrawableRes var icon: Int = R.drawable.ic_play_seek_triangle set(value) { @@ -87,9 +86,11 @@ class SecondsView(context: Context?, attrs: AttributeSet?) : icon_1.alpha = 0f icon_2.alpha = 0f icon_3.alpha = 0f - }, { + }, + { icon_1.alpha = it - }, { + }, + { secondAnimator.start() } ) @@ -99,9 +100,11 @@ class SecondsView(context: Context?, attrs: AttributeSet?) : icon_1.alpha = 1f icon_2.alpha = 0f icon_3.alpha = 0f - }, { + }, + { icon_2.alpha = it - }, { + }, + { thirdAnimator.start() } ) @@ -111,10 +114,12 @@ class SecondsView(context: Context?, attrs: AttributeSet?) : icon_1.alpha = 1f icon_2.alpha = 1f icon_3.alpha = 0f - }, { + }, + { icon_1.alpha = 1f - icon_3.alpha icon_3.alpha = it - }, { + }, + { fourthAnimator.start() } ) @@ -124,9 +129,11 @@ class SecondsView(context: Context?, attrs: AttributeSet?) : icon_1.alpha = 0f icon_2.alpha = 1f icon_3.alpha = 1f - }, { + }, + { icon_2.alpha = 1f - it - }, { + }, + { fifthAnimator.start() } ) @@ -136,16 +143,20 @@ class SecondsView(context: Context?, attrs: AttributeSet?) : icon_1.alpha = 0f icon_2.alpha = 0f icon_3.alpha = 1f - }, { + }, + { icon_3.alpha = 1f - it - }, { + }, + { firstAnimator.start() } ) private inner class CustomValueAnimator( - start: () -> Unit, update: (value: Float) -> Unit, end: () -> Unit - ): ValueAnimator() { + start: () -> Unit, + update: (value: Float) -> Unit, + end: () -> Unit + ) : ValueAnimator() { init { duration = cycleDuration / 5 @@ -164,7 +175,6 @@ class SecondsView(context: Context?, attrs: AttributeSet?) : override fun onAnimationCancel(animation: Animator?) = Unit override fun onAnimationRepeat(animation: Animator?) = Unit - }) } } diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index f3b1056d2..cdb5849ab 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -54,11 +54,19 @@ tools:ignore="ContentDescription" tools:visibility="visible" /> + + @@ -754,4 +762,11 @@ android:textColor="@color/white" android:visibility="gone" /> + + diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index c2d1c84ff..e71ffdd96 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -54,11 +54,19 @@ tools:ignore="ContentDescription" tools:visibility="visible" /> + + @@ -751,4 +759,11 @@ android:textColor="@color/white" android:visibility="gone" /> + + From 3aebfa22e9a04582dd4117064fe6b7c62be45960 Mon Sep 17 00:00:00 2001 From: vkay94 Date: Mon, 18 Jan 2021 23:12:03 +0100 Subject: [PATCH 03/21] SeekOverlay: Switch to merge for SecondsView and other adjustments --- .../main/java/org/schabi/newpipe/ktx/View.kt | 2 +- .../org/schabi/newpipe/player/Player.java | 20 +++++++++---------- .../newpipe/views/player/SecondsView.kt | 7 ++++--- app/src/main/res/layout-large-land/player.xml | 2 +- app/src/main/res/layout/player.xml | 2 +- .../res/layout/player_seek_seconds_view.xml | 13 ++++++------ 6 files changed, 24 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt index 5adb4e3ef..ae1b4388c 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt @@ -54,7 +54,7 @@ fun View.animate( ) Log.d(TAG, "animate(): $msg") } - if (isVisible && enterOrExit && alpha == 1f && animationType == AnimationType.ALPHA) { + if (isVisible && enterOrExit) { if (MainActivity.DEBUG) { Log.d(TAG, "animate(): view was already visible > view = [$this]") } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index de8bffaf7..14b5f9316 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -251,6 +251,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 @@ -586,10 +587,9 @@ public final class Player implements } private void setupPlayerSeekOverlay() { - final int fadeDurations = 450; binding.seekOverlay.showCircle(true) .circleBackgroundColorInt(CircleClipTapView.COLOR_DARK_TRANSPARENT) - .seekSeconds(retrieveSeekDurationFromPreferences(this) / 1000) + .seekSeconds((int) (retrieveSeekDurationFromPreferences(this) / 1000.0f)) .performListener(new PlayerSeekOverlay.PerformListener() { @Override @@ -605,22 +605,22 @@ public final class Player implements @Override public void onAnimationStart() { - animate(binding.seekOverlay, true, fadeDurations); + animate(binding.seekOverlay, true, SEEK_OVERLAY_DURATION); animate(binding.playbackControlsShadow, - !simpleExoPlayer.getPlayWhenReady(), fadeDurations); - animate(binding.playerTopShadow, false, fadeDurations); - animate(binding.playerBottomShadow, false, fadeDurations); - animate(binding.playbackControlRoot, false, fadeDurations); + !simpleExoPlayer.getPlayWhenReady(), SEEK_OVERLAY_DURATION); + animate(binding.playerTopShadow, false, SEEK_OVERLAY_DURATION); + animate(binding.playerBottomShadow, false, SEEK_OVERLAY_DURATION); + animate(binding.playbackControlRoot, false, SEEK_OVERLAY_DURATION); hideSystemUIIfNeeded(); } @Override public void onAnimationEnd() { - animate(binding.seekOverlay, false, fadeDurations); + animate(binding.seekOverlay, false, SEEK_OVERLAY_DURATION); if (!simpleExoPlayer.getPlayWhenReady()) { - showControls(fadeDurations); + showControls(SEEK_OVERLAY_DURATION); } else { - showHideShadow(false, fadeDurations); + showHideShadow(false, SEEK_OVERLAY_DURATION); } } diff --git a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt index fa2dbd63e..f01b116ce 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt @@ -5,14 +5,13 @@ import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater +import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.DrawableRes -import androidx.constraintlayout.widget.ConstraintLayout import kotlinx.android.synthetic.main.player_seek_seconds_view.view.* import org.schabi.newpipe.R -class SecondsView(context: Context, attrs: AttributeSet?) : - ConstraintLayout(context, attrs) { +class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { companion object { const val ICON_ANIMATION_DURATION = 750L @@ -58,6 +57,8 @@ class SecondsView(context: Context, attrs: AttributeSet?) : init { LayoutInflater.from(context).inflate(R.layout.player_seek_seconds_view, this, true) + orientation = VERTICAL + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } fun start() { diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index cdb5849ab..10f5a60a0 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -767,6 +767,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" - android:alpha="0" /> + android:alpha="0" /> diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index e71ffdd96..3625e0e69 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -764,6 +764,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" - android:alpha="0" /> + android:alpha="0" /> diff --git a/app/src/main/res/layout/player_seek_seconds_view.xml b/app/src/main/res/layout/player_seek_seconds_view.xml index 14c9eaa2d..8aed15594 100644 --- a/app/src/main/res/layout/player_seek_seconds_view.xml +++ b/app/src/main/res/layout/player_seek_seconds_view.xml @@ -1,11 +1,12 @@ - + tools:ignore="ContentDescription" + tools:layout_height="wrap_content" + tools:layout_width="match_parent" + tools:orientation="vertical" + tools:parentTag="android.widget.LinearLayout"> - + From ee827407aafeb3b2d05fe5fd9afc806da66f864e Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 31 Jul 2021 10:33:57 +0200 Subject: [PATCH 04/21] Fix seek triangles not visible on API 19 --- app/src/main/res/layout/player_seek_seconds_view.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/player_seek_seconds_view.xml b/app/src/main/res/layout/player_seek_seconds_view.xml index 8aed15594..57f5aa787 100644 --- a/app/src/main/res/layout/player_seek_seconds_view.xml +++ b/app/src/main/res/layout/player_seek_seconds_view.xml @@ -15,21 +15,21 @@ android:gravity="center_horizontal" android:orientation="horizontal"> - - - Date: Tue, 31 Aug 2021 19:08:40 +0200 Subject: [PATCH 05/21] Convert SecondsView from kotlin synthetics to view binding --- .../newpipe/views/player/SecondsView.kt | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt index f01b116ce..8574d607f 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt @@ -8,8 +8,8 @@ import android.view.LayoutInflater import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.DrawableRes -import kotlinx.android.synthetic.main.player_seek_seconds_view.view.* import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.PlayerSeekSecondsViewBinding class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { @@ -29,7 +29,7 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context var seconds: Int = 0 set(value) { - tv_seconds.text = context.resources.getQuantityString( + binding.tvSeconds.text = context.resources.getQuantityString( R.plurals.seconds, value, value ) field = value @@ -37,26 +37,27 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context var isForward: Boolean = true set(value) { - triangle_container.rotation = if (value) 0f else 180f + binding.triangleContainer.rotation = if (value) 0f else 180f field = value } + val binding = PlayerSeekSecondsViewBinding.inflate(LayoutInflater.from(context), this) + val textView: TextView - get() = tv_seconds + get() = binding.tvSeconds @DrawableRes var icon: Int = R.drawable.ic_play_seek_triangle set(value) { if (value > 0) { - icon_1.setImageResource(value) - icon_2.setImageResource(value) - icon_3.setImageResource(value) + binding.icon1.setImageResource(value) + binding.icon2.setImageResource(value) + binding.icon3.setImageResource(value) } field = value } init { - LayoutInflater.from(context).inflate(R.layout.player_seek_seconds_view, this, true) orientation = VERTICAL layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } @@ -77,19 +78,19 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context } private fun reset() { - icon_1.alpha = 0f - icon_2.alpha = 0f - icon_3.alpha = 0f + binding.icon1.alpha = 0f + binding.icon2.alpha = 0f + binding.icon3.alpha = 0f } private val firstAnimator: ValueAnimator = CustomValueAnimator( { - icon_1.alpha = 0f - icon_2.alpha = 0f - icon_3.alpha = 0f + binding.icon1.alpha = 0f + binding.icon2.alpha = 0f + binding.icon3.alpha = 0f }, { - icon_1.alpha = it + binding.icon1.alpha = it }, { secondAnimator.start() @@ -98,12 +99,12 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context private val secondAnimator: ValueAnimator = CustomValueAnimator( { - icon_1.alpha = 1f - icon_2.alpha = 0f - icon_3.alpha = 0f + binding.icon1.alpha = 1f + binding.icon2.alpha = 0f + binding.icon3.alpha = 0f }, { - icon_2.alpha = it + binding.icon2.alpha = it }, { thirdAnimator.start() @@ -112,13 +113,13 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context private val thirdAnimator: ValueAnimator = CustomValueAnimator( { - icon_1.alpha = 1f - icon_2.alpha = 1f - icon_3.alpha = 0f + binding.icon1.alpha = 1f + binding.icon2.alpha = 1f + binding.icon3.alpha = 0f }, { - icon_1.alpha = 1f - icon_3.alpha - icon_3.alpha = it + binding.icon1.alpha = 1f - binding.icon3.alpha + binding.icon3.alpha = it }, { fourthAnimator.start() @@ -127,12 +128,12 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context private val fourthAnimator: ValueAnimator = CustomValueAnimator( { - icon_1.alpha = 0f - icon_2.alpha = 1f - icon_3.alpha = 1f + binding.icon1.alpha = 0f + binding.icon2.alpha = 1f + binding.icon3.alpha = 1f }, { - icon_2.alpha = 1f - it + binding.icon2.alpha = 1f - it }, { fifthAnimator.start() @@ -141,12 +142,12 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context private val fifthAnimator: ValueAnimator = CustomValueAnimator( { - icon_1.alpha = 0f - icon_2.alpha = 0f - icon_3.alpha = 1f + binding.icon1.alpha = 0f + binding.icon2.alpha = 0f + binding.icon3.alpha = 1f }, { - icon_3.alpha = 1f - it + binding.icon3.alpha = 1f - it }, { firstAnimator.start() From a454a41b51bd6c0a154225b1e1e8174238dcbcd6 Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 1 Sep 2021 17:24:10 +0200 Subject: [PATCH 06/21] Fix controls not hiding correctly when switching player --- app/src/main/java/org/schabi/newpipe/player/Player.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 14b5f9316..b859d9997 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -2186,8 +2186,8 @@ public final class Player implements 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() From 03d53725258a55234fad5a66d29e3c2e205a4ebc Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 1 Sep 2021 19:51:52 +0200 Subject: [PATCH 07/21] Fix buttons' selectable item background not working in player --- app/src/main/res/layout-large-land/player.xml | 2 ++ app/src/main/res/layout/player.xml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index 10f5a60a0..5ffb7e0fb 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -63,10 +63,12 @@ android:background="@color/video_overlay_color" tools:visibility="visible" /> + diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index 3625e0e69..22817c4e6 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -63,10 +63,12 @@ android:background="@color/video_overlay_color" tools:visibility="visible" /> + From 83a3d11f38154b03d6fa398bf375d8ad088308a9 Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 1 Sep 2021 18:34:37 +0200 Subject: [PATCH 08/21] Small improvements to player --- .../java/org/schabi/newpipe/player/Player.java | 1 + app/src/main/res/layout-large-land/player.xml | 18 +++++++++--------- app/src/main/res/layout/player.xml | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index b859d9997..467d1e757 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -533,6 +533,7 @@ public final class Player implements playerGestureListener = new PlayerGestureListener(this, service); gestureDetector = new GestureDetectorCompat(context, playerGestureListener); + //noinspection ClickableViewAccessibility binding.getRoot().setOnTouchListener(playerGestureListener); setupPlayerSeekOverlay(); diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index 5ffb7e0fb..520c165cb 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -58,9 +58,9 @@ 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" + android:visibility="gone" tools:visibility="visible" /> @@ -479,8 +479,8 @@ android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitCenter" android:visibility="gone" - app:tint="@color/white" app:srcCompat="@drawable/ic_fullscreen" + app:tint="@color/white" tools:ignore="ContentDescription,RtlHardcoded" tools:visibility="visible" /> @@ -503,8 +503,8 @@ android:clickable="true" android:focusable="true" android:scaleType="fitCenter" - app:tint="@color/white" app:srcCompat="@drawable/ic_previous" + app:tint="@color/white" tools:ignore="ContentDescription" /> @@ -515,8 +515,8 @@ android:layout_weight="1" android:background="?attr/selectableItemBackgroundBorderless" android:scaleType="fitCenter" - app:tint="@color/white" app:srcCompat="@drawable/ic_pause" + app:tint="@color/white" tools:ignore="ContentDescription" /> @@ -582,8 +582,8 @@ android:focusable="true" android:padding="10dp" android:scaleType="fitXY" - app:tint="@color/white" - app:srcCompat="@drawable/ic_close" /> + app:srcCompat="@drawable/ic_close" + app:tint="@color/white" /> + android:alpha="0" + android:visibility="invisible" /> diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index 22817c4e6..27b8936a3 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -765,7 +765,7 @@ android:id="@+id/seekOverlay" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="invisible" - android:alpha="0" /> + android:alpha="0" + android:visibility="invisible" /> From dac47d9f5244ff1d159e9ec0edfbc90e1c94b106 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Mon, 11 Oct 2021 23:02:36 +0200 Subject: [PATCH 09/21] Replace NotNull annotation with NonNull annotation --- app/src/main/java/org/schabi/newpipe/player/Player.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 467d1e757..8e340302a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -136,7 +136,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; -import org.jetbrains.annotations.NotNull; import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; @@ -626,7 +625,7 @@ public final class Player implements } @Override - public Boolean shouldFastForward(@NotNull final DisplayPortion portion) { + public Boolean shouldFastForward(@NonNull final DisplayPortion portion) { // Null indicates an invalid area or condition e.g. the middle portion // or video start or end was reached during double tap seeking if (invalidSeekConditions()) { From fe42206e94913c3a35301ac10de2b29e333f98ee Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 3 Dec 2021 20:29:18 +0100 Subject: [PATCH 10/21] Code cleanup and minimization * Deduplicated and simplified a lot of code * Fixed ``invalidSeekConditions`` so that it's possible to seek while the player is loading (like currently the case) --- .../org/schabi/newpipe/player/Player.java | 129 ++------ .../player/event/BasePlayerGestureListener.kt | 6 +- .../player/event/PlayerGestureListener.java | 3 +- .../newpipe/views/player/CircleClipTapView.kt | 33 +-- .../views/player/PlayerFastSeekOverlay.kt | 136 +++++++++ .../newpipe/views/player/PlayerSeekOverlay.kt | 278 ------------------ .../newpipe/views/player/SecondsView.kt | 30 +- app/src/main/res/layout-large-land/player.xml | 22 +- app/src/main/res/layout/player.xml | 22 +- ...erlay.xml => player_fast_seek_overlay.xml} | 3 +- ....xml => player_fast_seek_seconds_view.xml} | 0 11 files changed, 185 insertions(+), 477 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt rename app/src/main/res/layout/{player_seek_overlay.xml => player_fast_seek_overlay.xml} (89%) rename app/src/main/res/layout/{player_seek_seconds_view.xml => player_fast_seek_seconds_view.xml} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 8e340302a..fbfaf7b95 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -51,8 +51,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; 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; @@ -189,8 +187,7 @@ import org.schabi.newpipe.util.StreamTypeUtil; 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.CircleClipTapView; -import org.schabi.newpipe.views.player.PlayerSeekOverlay; +import org.schabi.newpipe.views.player.PlayerFastSeekOverlay; import java.io.IOException; import java.util.ArrayList; @@ -454,6 +451,8 @@ public final class Player implements initPlayer(true); } initListeners(); + + setupPlayerSeekOverlay(); } private void initViews(@NonNull final PlayerBinding playerBinding) { @@ -530,12 +529,6 @@ public final class Player implements binding.resizeTextView.setOnClickListener(this); binding.playbackLiveSync.setOnClickListener(this); - playerGestureListener = new PlayerGestureListener(this, service); - gestureDetector = new GestureDetectorCompat(context, playerGestureListener); - //noinspection ClickableViewAccessibility - binding.getRoot().setOnTouchListener(playerGestureListener); - setupPlayerSeekOverlay(); - binding.queueButton.setOnClickListener(this); binding.segmentsButton.setOnClickListener(this); binding.repeatButton.setOnClickListener(this); @@ -586,26 +579,26 @@ public final class Player implements v.getPaddingBottom())); } + /** + * Initializes the Fast-For/Backward overlay. + */ private void setupPlayerSeekOverlay() { - binding.seekOverlay.showCircle(true) - .circleBackgroundColorInt(CircleClipTapView.COLOR_DARK_TRANSPARENT) + playerGestureListener = new PlayerGestureListener(this, service); + gestureDetector = new GestureDetectorCompat(context, playerGestureListener); + binding.getRoot().setOnTouchListener(playerGestureListener); + + binding.fastSeekOverlay .seekSeconds((int) (retrieveSeekDurationFromPreferences(this) / 1000.0f)) - .performListener(new PlayerSeekOverlay.PerformListener() { + .performListener(new PlayerFastSeekOverlay.PerformListener() { @Override - public void onPrepare() { - if (invalidSeekConditions()) { - playerGestureListener.endMultiDoubleTap(); - return; - } - binding.seekOverlay.arcSize( - CircleClipTapView.Companion.calculateArcSize(getSurfaceView()) - ); + public void onDoubleTabStart() { + // TODO } @Override - public void onAnimationStart() { - animate(binding.seekOverlay, true, SEEK_OVERLAY_DURATION); + public void onDoubleTab() { + animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION); animate(binding.playbackControlsShadow, !simpleExoPlayer.getPlayWhenReady(), SEEK_OVERLAY_DURATION); animate(binding.playerTopShadow, false, SEEK_OVERLAY_DURATION); @@ -615,8 +608,8 @@ public final class Player implements } @Override - public void onAnimationEnd() { - animate(binding.seekOverlay, false, SEEK_OVERLAY_DURATION); + public void onDoubleTabEnd() { + animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION); if (!simpleExoPlayer.getPlayWhenReady()) { showControls(SEEK_OVERLAY_DURATION); } else { @@ -629,6 +622,7 @@ public final class Player implements // Null indicates an invalid area or condition e.g. the middle portion // or video start or end was reached during double tap seeking if (invalidSeekConditions()) { + playerGestureListener.endMultiDoubleTap(); return null; } if (portion == DisplayPortion.LEFT @@ -637,9 +631,9 @@ public final class Player implements return false; } else if (portion == DisplayPortion.RIGHT) { return true; - } else /* portion == DisplayPortion.MIDDLE */ { - return null; } + /* portion == DisplayPortion.MIDDLE */ + return null; } @Override @@ -653,12 +647,13 @@ public final class Player implements } private boolean invalidSeekConditions() { - return simpleExoPlayer.getCurrentPosition() == simpleExoPlayer.getDuration() - || currentState == STATE_COMPLETED - || !isPrepared; + return exoPlayerIsNull() + || simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_ENDED + || simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration() + || currentState == STATE_COMPLETED; } }); - playerGestureListener.doubleTapControls(binding.seekOverlay); + playerGestureListener.doubleTapControls(binding.fastSeekOverlay); } //endregion @@ -1879,71 +1874,6 @@ public final class Player implements 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"); @@ -2214,8 +2144,6 @@ public final class Player implements updateStreamRelatedViews(); - showAndAnimateControl(-1, true); - binding.playbackSeekBar.setEnabled(true); binding.playbackSeekBar.getThumb() .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); @@ -2295,7 +2223,6 @@ public final class Player implements if (DEBUG) { Log.d(TAG, "onPausedSeek() called"); } - showAndAnimateControl(-1, true); animatePlayButtons(false, 100); binding.getRoot().setKeepScreenOn(true); @@ -4364,8 +4291,8 @@ public final class Player implements return binding.currentDisplaySeek; } - public PlayerSeekOverlay getSeekOverlay() { - return binding.seekOverlay; + public PlayerFastSeekOverlay getFastSeekOverlay() { + return binding.fastSeekOverlay; } @Nullable diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt index 29ae7c5c3..c89eabb47 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt +++ b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt @@ -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 @@ -459,10 +459,6 @@ abstract class BasePlayerGestureListener( doubleTapControls?.onDoubleTapFinished() } - fun enableMultiDoubleTap(enable: Boolean) = apply { - doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0 - } - // /////////////////////////////////////////////////////////////////// // Utils // /////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index b215584e8..794fe9b3c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -230,11 +230,10 @@ public class PlayerGestureListener if (DEBUG) { Log.d(TAG, "onPopupResizingStart called"); } - player.showAndAnimateControl(-1, true); player.getLoadingPanel().setVisibility(View.GONE); player.hideControls(0, 0); - animate(player.getSeekOverlay(), false, 0); + animate(player.getFastSeekOverlay(), false, 0); animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0); } diff --git a/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt index 6b22538e0..e3d142916 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt @@ -6,18 +6,9 @@ import android.graphics.Paint import android.graphics.Path import android.util.AttributeSet import android.view.View -import org.schabi.newpipe.player.event.DisplayPortion class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, attrs) { - companion object { - const val COLOR_DARK = 0x45000000 - const val COLOR_DARK_TRANSPARENT = 0x30000000 - const val COLOR_LIGHT_TRANSPARENT = 0x25EEEEEE - - fun calculateArcSize(view: View): Float = view.height / 11.4f - } - private var backgroundPaint = Paint() private var widthPx = 0 @@ -26,6 +17,7 @@ class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, // Background private var shapePath = Path() + private var arcSize: Float = 80f private var isLeft = true init { @@ -34,7 +26,7 @@ class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, backgroundPaint.apply { style = Paint.Style.FILL isAntiAlias = true - color = COLOR_LIGHT_TRANSPARENT + color = 0x30000000 } val dm = context.resources.displayMetrics @@ -44,24 +36,15 @@ class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, updatePathShape() } - var arcSize: Float = 80f - set(value) { - field = value + fun updateArcSize(baseView: View) { + val newArcSize = baseView.height / 11.4f + if (arcSize != newArcSize) { + arcSize = newArcSize updatePathShape() } + } - var circleBackgroundColor: Int - get() = backgroundPaint.color - set(value) { - backgroundPaint.color = value - } - - /* - Background - */ - - fun updatePosition(portion: DisplayPortion) { - val newIsLeft = portion == DisplayPortion.LEFT + fun updatePosition(newIsLeft: Boolean) { if (isLeft != newIsLeft) { isLeft = newIsLeft updatePathShape() diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt new file mode 100644 index 000000000..90384264b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -0,0 +1,136 @@ +package org.schabi.newpipe.views.player + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +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 + } + + var seekSeconds: Int = 0 + private set + + fun seekSeconds(seconds: Int) = apply { + seekSeconds = seconds + } + + // 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 + performListener?.onDoubleTabStart() + + secondsView.stop() + } + + override fun onDoubleTapProgressDown(portion: DisplayPortion) { + val shouldForward: Boolean = performListener?.shouldFastForward(portion) ?: return + + if (DEBUG) + Log.d( + TAG, + "onDoubleTapProgressDown called with " + + "shouldForward = [$shouldForward], " + + "wasForwarding = [$wasForwarding], " + + "initTap = [$initTap], " + ) + + /* + * Check if a initial tab 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) { + // Start animation + secondsView.start() + initTap = true + } + } + + performListener?.onDoubleTab() + + secondsView.seconds += seekSeconds + performListener?.seek(forward = shouldForward) + } + + override fun onDoubleTapFinished() { + if (DEBUG) + Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]") + + if (initTap) performListener?.onDoubleTabEnd() + initTap = false + } + + 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.start() + applyTo(rootConstraintLayout) + } + } + + interface PerformListener { + fun onDoubleTabStart() {} + fun onDoubleTab() + fun onDoubleTabEnd() + fun shouldFastForward(portion: DisplayPortion): Boolean? + fun seek(forward: Boolean) + } + + companion object { + private const val TAG = "PlayerSeekOverlay" + private val DEBUG = MainActivity.DEBUG + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt deleted file mode 100644 index d61989d92..000000000 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerSeekOverlay.kt +++ /dev/null @@ -1,278 +0,0 @@ -package org.schabi.newpipe.views.player - -import android.content.Context -import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DimenRes -import androidx.annotation.DrawableRes -import androidx.annotation.StyleRes -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 androidx.core.content.ContextCompat -import androidx.core.widget.TextViewCompat -import androidx.preference.PreferenceManager -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 PlayerSeekOverlay(context: Context, private val attrs: AttributeSet?) : - ConstraintLayout(context, attrs), DoubleTapListener { - - private var secondsView: SecondsView - private var circleClipTapView: CircleClipTapView - private var rootConstraintLayout: ConstraintLayout - - private var isForwarding: Boolean? = null - - init { - LayoutInflater.from(context).inflate(R.layout.player_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) - - initializeAttributes() - secondsView.isForward = true - isForwarding = null - changeConstraints(true) - } - - private fun initializeAttributes() { - circleBackgroundColorInt(CircleClipTapView.COLOR_LIGHT_TRANSPARENT) - iconAnimationDuration(SecondsView.ICON_ANIMATION_DURATION) - icon(R.drawable.ic_play_seek_triangle) - - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - val durationKey = context.getString(R.string.seek_duration_key) - val seekValue = prefs.getString( - durationKey, context.getString(R.string.seek_duration_default_value) - ) - seekSeconds(seekValue?.toInt()?.div(1000) ?: 10) - } - - private var performListener: PerformListener? = null - - fun performListener(listener: PerformListener) = apply { - performListener = listener - } - - var seekSeconds: Int = 0 - private set - - fun seekSeconds(seconds: Int) = apply { - seekSeconds = seconds - } - - var circleBackgroundColor: Int - get() = circleClipTapView.circleBackgroundColor - private set(value) { - circleClipTapView.circleBackgroundColor = value - } - - fun circleBackgroundColorRes(@ColorRes resId: Int) = apply { - circleBackgroundColor = ContextCompat.getColor(context, resId) - } - - fun circleBackgroundColorInt(@ColorInt color: Int) = apply { - circleBackgroundColor = color - } - - var arcSize: Float - get() = circleClipTapView.arcSize - internal set(value) { - circleClipTapView.arcSize = value - } - - fun arcSize(@DimenRes resId: Int) = apply { - arcSize = context.resources.getDimension(resId) - } - - fun arcSize(px: Float) = apply { - arcSize = px - } - - var showCircle: Boolean = true - private set(value) { - circleClipTapView.visibility = if (value) View.VISIBLE else View.GONE - field = value - } - - fun showCircle(show: Boolean) = apply { - showCircle = show - } - - var iconAnimationDuration: Long = SecondsView.ICON_ANIMATION_DURATION - get() = secondsView.cycleDuration - private set(value) { - secondsView.cycleDuration = value - field = value - } - - fun iconAnimationDuration(duration: Long) = apply { - iconAnimationDuration = duration - } - - @DrawableRes - var icon: Int = 0 - get() = secondsView.icon - private set(value) { - secondsView.stop() - secondsView.icon = value - field = value - } - - fun icon(@DrawableRes resId: Int) = apply { - icon = resId - } - - @StyleRes - var textAppearance: Int = 0 - private set(value) { - TextViewCompat.setTextAppearance(secondsView.textView, value) - field = value - } - - fun textAppearance(@StyleRes resId: Int) = apply { - textAppearance = resId - } - - // 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 - performListener?.onPrepare() - - changeConstraints(secondsView.isForward) - if (showCircle) circleClipTapView.updatePosition(portion) - - isForwarding = null - - if (this.alpha == 0f) - secondsView.stop() - } - - override fun onDoubleTapProgressDown(portion: DisplayPortion) { - val shouldForward: Boolean = performListener?.shouldFastForward(portion) ?: return - - if (DEBUG) - Log.d( - TAG, - "onDoubleTapProgressDown called with " + - "shouldForward = [$shouldForward], " + - "isForwarding = [$isForwarding], " + - "secondsView#isForward = [${secondsView.isForward}], " + - "initTap = [$initTap], " - ) - - // Using this check prevents from fast switching (one touches) - if (isForwarding != null && isForwarding != shouldForward) { - isForwarding = shouldForward - return - } - isForwarding = shouldForward - - if (this.visibility != View.VISIBLE || !initTap) { - if (!initTap) { - secondsView.seconds = 0 - performListener?.onAnimationStart() - secondsView.start() - initTap = true - } - } - - if (shouldForward) - forwarding() - else - rewinding() - } - - override fun onDoubleTapFinished() { - if (DEBUG) - Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]") - - if (initTap) performListener?.onAnimationEnd() - initTap = false - } - - private fun forwarding() { - if (DEBUG) - Log.d(TAG, "forwarding called") - - // First time tap or switched - if (!secondsView.isForward) { - changeConstraints(true) - if (showCircle) circleClipTapView.updatePosition(DisplayPortion.RIGHT) - secondsView.apply { - isForward = true - seconds = 0 - } - } - secondsView.seconds += seekSeconds - performListener?.seek(forward = true) - } - - private fun rewinding() { - if (DEBUG) - Log.d(TAG, "rewinding called") - - // First time tap or switched - if (secondsView.isForward) { - changeConstraints(false) - if (showCircle) circleClipTapView.updatePosition(DisplayPortion.LEFT) - secondsView.apply { - isForward = false - seconds = 0 - } - } - secondsView.seconds += seekSeconds - performListener?.seek(forward = false) - } - - private fun changeConstraints(forward: Boolean) { - val constraintSet = ConstraintSet() - with(constraintSet) { - clone(rootConstraintLayout) - if (forward) { - clear(secondsView.id, START) - connect( - secondsView.id, END, - PARENT_ID, END - ) - } else { - clear(secondsView.id, END) - connect( - secondsView.id, START, - PARENT_ID, START - ) - } - secondsView.start() - applyTo(rootConstraintLayout) - } - } - - interface PerformListener { - fun onPrepare() {} - fun onAnimationStart() - fun onAnimationEnd() - fun shouldFastForward(portion: DisplayPortion): Boolean? - fun seek(forward: Boolean) - } - - companion object { - private const val TAG = "PlayerSeekOverlay" - private val DEBUG = MainActivity.DEBUG - } -} diff --git a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt index 8574d607f..69dd09a07 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt @@ -6,10 +6,8 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout -import android.widget.TextView -import androidx.annotation.DrawableRes import org.schabi.newpipe.R -import org.schabi.newpipe.databinding.PlayerSeekSecondsViewBinding +import org.schabi.newpipe.databinding.PlayerFastSeekSecondsViewBinding class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { @@ -35,33 +33,17 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context field = value } - var isForward: Boolean = true - set(value) { - binding.triangleContainer.rotation = if (value) 0f else 180f - field = value - } - - val binding = PlayerSeekSecondsViewBinding.inflate(LayoutInflater.from(context), this) - - val textView: TextView - get() = binding.tvSeconds - - @DrawableRes - var icon: Int = R.drawable.ic_play_seek_triangle - set(value) { - if (value > 0) { - binding.icon1.setImageResource(value) - binding.icon2.setImageResource(value) - binding.icon3.setImageResource(value) - } - field = value - } + 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 start() { stop() firstAnimator.start() diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index 520c165cb..71a325cf3 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -647,24 +647,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - - - - - - - + android:focusable="false" /> Date: Tue, 14 Dec 2021 20:44:01 +0100 Subject: [PATCH 11/21] Refactored code --- app/src/main/java/org/schabi/newpipe/player/Player.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index fbfaf7b95..653b226df 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -529,6 +529,10 @@ public final class Player implements binding.resizeTextView.setOnClickListener(this); binding.playbackLiveSync.setOnClickListener(this); + playerGestureListener = new PlayerGestureListener(this, service); + gestureDetector = new GestureDetectorCompat(context, playerGestureListener); + binding.getRoot().setOnTouchListener(playerGestureListener); + binding.queueButton.setOnClickListener(this); binding.segmentsButton.setOnClickListener(this); binding.repeatButton.setOnClickListener(this); @@ -583,10 +587,6 @@ public final class Player implements * Initializes the Fast-For/Backward overlay. */ private void setupPlayerSeekOverlay() { - playerGestureListener = new PlayerGestureListener(this, service); - gestureDetector = new GestureDetectorCompat(context, playerGestureListener); - binding.getRoot().setOnTouchListener(playerGestureListener); - binding.fastSeekOverlay .seekSeconds((int) (retrieveSeekDurationFromPreferences(this) / 1000.0f)) .performListener(new PlayerFastSeekOverlay.PerformListener() { From c25e523df6cbb0a39893d4d1ca4b68248a4ddb5e Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 14 Dec 2021 20:44:39 +0100 Subject: [PATCH 12/21] Removed all animations to be consistent with the current behavior --- .../java/org/schabi/newpipe/player/Player.java | 16 ---------------- .../views/player/PlayerFastSeekOverlay.kt | 2 -- 2 files changed, 18 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 653b226df..8eadbc821 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -591,30 +591,14 @@ public final class Player implements .seekSeconds((int) (retrieveSeekDurationFromPreferences(this) / 1000.0f)) .performListener(new PlayerFastSeekOverlay.PerformListener() { - @Override - public void onDoubleTabStart() { - // TODO - } - @Override public void onDoubleTab() { animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION); - animate(binding.playbackControlsShadow, - !simpleExoPlayer.getPlayWhenReady(), SEEK_OVERLAY_DURATION); - animate(binding.playerTopShadow, false, SEEK_OVERLAY_DURATION); - animate(binding.playerBottomShadow, false, SEEK_OVERLAY_DURATION); - animate(binding.playbackControlRoot, false, SEEK_OVERLAY_DURATION); - hideSystemUIIfNeeded(); } @Override public void onDoubleTabEnd() { animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION); - if (!simpleExoPlayer.getPlayWhenReady()) { - showControls(SEEK_OVERLAY_DURATION); - } else { - showHideShadow(false, SEEK_OVERLAY_DURATION); - } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index 90384264b..f185f5861 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -57,7 +57,6 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : Log.d(TAG, "onDoubleTapStarted called with portion = [$portion]") initTap = false - performListener?.onDoubleTabStart() secondsView.stop() } @@ -122,7 +121,6 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : } interface PerformListener { - fun onDoubleTabStart() {} fun onDoubleTab() fun onDoubleTabEnd() fun shouldFastForward(portion: DisplayPortion): Boolean? From 452fe3a8e2b8b0824a35347679b6617055e142a1 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 14 Dec 2021 20:52:46 +0100 Subject: [PATCH 13/21] Respect disabled animations --- .../views/player/PlayerFastSeekOverlay.kt | 8 +++---- .../newpipe/views/player/SecondsView.kt | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index f185f5861..a1ea3d901 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -58,7 +58,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : initTap = false - secondsView.stop() + secondsView.stopAnimation() } override fun onDoubleTapProgressDown(portion: DisplayPortion) { @@ -86,8 +86,6 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : wasForwarding = shouldForward if (!initTap) { - // Start animation - secondsView.start() initTap = true } } @@ -104,6 +102,8 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : if (initTap) performListener?.onDoubleTabEnd() initTap = false + + secondsView.stopAnimation() } private fun changeConstraints(forward: Boolean) { @@ -115,7 +115,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : secondsView.id, if (forward) END else START, PARENT_ID, if (forward) END else START ) - secondsView.start() + secondsView.startAnimation() applyTo(rootConstraintLayout) } } diff --git a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt index 69dd09a07..d209d24da 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt @@ -8,6 +8,7 @@ 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) { @@ -33,6 +34,9 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context 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 { @@ -44,12 +48,18 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context binding.triangleContainer.rotation = if (isForward) 0f else 180f } - fun start() { - stop() - firstAnimator.start() + fun startAnimation() { + stopAnimation() + + if (animationsEnabled) { + firstAnimator.start() + } else { + // If no animations are enable show the arrow(s) without animation + showWithoutAnimation() + } } - fun stop() { + fun stopAnimation() { firstAnimator.cancel() secondAnimator.cancel() thirdAnimator.cancel() @@ -65,6 +75,12 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context 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 From 7bf1f3dba6c84e11e994a3603e419eafa85764df Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:09:15 +0100 Subject: [PATCH 14/21] Removed unused field --- app/src/main/java/org/schabi/newpipe/player/Player.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 8eadbc821..a869de4b0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -314,7 +314,6 @@ public final class Player implements private PlayerBinding binding; - private ValueAnimator controlViewAnimator; private final Handler controlsVisibilityHandler = new Handler(); // fullscreen player From d2aaf152a09dbaf9ee56bcd2ca53eadb25c5bad6 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:14:41 +0100 Subject: [PATCH 15/21] Removed related import --- app/src/main/java/org/schabi/newpipe/player/Player.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index a869de4b0..6352847d3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -51,7 +51,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; From 345ba74d588325167fb9efcccd0a996b063cc981 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 31 Dec 2021 20:58:23 +0100 Subject: [PATCH 16/21] Fixed naming --- .../main/java/org/schabi/newpipe/player/Player.java | 4 ++-- .../newpipe/views/player/PlayerFastSeekOverlay.kt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 6352847d3..b37dddc7b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -590,12 +590,12 @@ public final class Player implements .performListener(new PlayerFastSeekOverlay.PerformListener() { @Override - public void onDoubleTab() { + public void onDoubleTap() { animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION); } @Override - public void onDoubleTabEnd() { + public void onDoubleTapEnd() { animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION); } diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index a1ea3d901..23f56fa67 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -74,7 +74,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : ) /* - * Check if a initial tab occurred or if direction was switched + * Check if a initial tap occurred or if direction was switched */ if (!initTap || wasForwarding != shouldForward) { // Reset seconds and update position @@ -90,7 +90,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : } } - performListener?.onDoubleTab() + performListener?.onDoubleTap() secondsView.seconds += seekSeconds performListener?.seek(forward = shouldForward) @@ -100,7 +100,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : if (DEBUG) Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]") - if (initTap) performListener?.onDoubleTabEnd() + if (initTap) performListener?.onDoubleTapEnd() initTap = false secondsView.stopAnimation() @@ -121,8 +121,8 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : } interface PerformListener { - fun onDoubleTab() - fun onDoubleTabEnd() + fun onDoubleTap() + fun onDoubleTapEnd() fun shouldFastForward(portion: DisplayPortion): Boolean? fun seek(forward: Boolean) } From f8c52c4dacc89a4dd478a19004191102b2850768 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 31 Dec 2021 21:22:02 +0100 Subject: [PATCH 17/21] Fixed SonarLint problems * Removed alphaRelativeDuration as there is no use for it --- app/src/main/java/org/schabi/newpipe/ktx/View.kt | 8 +------- .../java/org/schabi/newpipe/player/Player.java | 15 +++++++++------ .../newpipe/views/player/PlayerFastSeekOverlay.kt | 6 ++++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt index ae1b4388c..8dcc9d85c 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt @@ -76,14 +76,8 @@ fun View.animate( animate().setListener(null).cancel() isVisible = true - val alphaRelativeDuration = if (enterOrExit && alpha < 1.0f) { - (duration * (1 - alpha)).toLong() - } else { - (duration * alpha).toLong() - } - when (animationType) { - AnimationType.ALPHA -> animateAlpha(enterOrExit, alphaRelativeDuration, delay, execOnEnd) + AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd) AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd) AnimationType.LIGHT_SCALE_AND_ALPHA -> animateLightScaleAndAlpha(enterOrExit, duration, delay, execOnEnd) AnimationType.SLIDE_AND_ALPHA -> animateSlideAndAlpha(enterOrExit, duration, delay, execOnEnd) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index b37dddc7b..f7ef13d7a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -600,22 +600,24 @@ public final class Player implements } @Override - public Boolean shouldFastForward(@NonNull final DisplayPortion portion) { + public Optional shouldFastForward( + @NonNull final DisplayPortion portion + ) { // Null indicates an invalid area or condition e.g. the middle portion // or video start or end was reached during double tap seeking if (invalidSeekConditions()) { playerGestureListener.endMultiDoubleTap(); - return null; + return Optional.empty(); } if (portion == DisplayPortion.LEFT // Small puffer to eliminate infinite rewind seeking && simpleExoPlayer.getCurrentPosition() > 500L) { - return false; + return Optional.of(false); } else if (portion == DisplayPortion.RIGHT) { - return true; + return Optional.of(true); } /* portion == DisplayPortion.MIDDLE */ - return null; + return Optional.empty(); } @Override @@ -630,7 +632,8 @@ public final class Player implements private boolean invalidSeekConditions() { return exoPlayerIsNull() - || simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_ENDED + || simpleExoPlayer.getPlaybackState() + == com.google.android.exoplayer2.Player.STATE_ENDED || simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration() || currentState == STATE_COMPLETED; } diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index 23f56fa67..5a3eb9a4e 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -13,6 +13,7 @@ import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R import org.schabi.newpipe.player.event.DisplayPortion import org.schabi.newpipe.player.event.DoubleTapListener +import java.util.Optional class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs), DoubleTapListener { @@ -62,7 +63,8 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : } override fun onDoubleTapProgressDown(portion: DisplayPortion) { - val shouldForward: Boolean = performListener?.shouldFastForward(portion) ?: return + val shouldForward: Boolean = + performListener?.shouldFastForward(portion)?.orElse(null) ?: return if (DEBUG) Log.d( @@ -123,7 +125,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : interface PerformListener { fun onDoubleTap() fun onDoubleTapEnd() - fun shouldFastForward(portion: DisplayPortion): Boolean? + fun shouldFastForward(portion: DisplayPortion): Optional fun seek(forward: Boolean) } From 1c20eabb48f4c9de518a8856f1c8ead2bf3138bc Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 6 Jan 2022 14:39:12 +0100 Subject: [PATCH 18/21] Code cleanup --- .../java/org/schabi/newpipe/player/Player.java | 10 +++++----- .../views/player/PlayerFastSeekOverlay.kt | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index f7ef13d7a..431b4bb98 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -600,24 +600,24 @@ public final class Player implements } @Override - public Optional shouldFastForward( + public FastSeekDirection getFastSeekDirection( @NonNull final DisplayPortion portion ) { // Null indicates an invalid area or condition e.g. the middle portion // or video start or end was reached during double tap seeking if (invalidSeekConditions()) { playerGestureListener.endMultiDoubleTap(); - return Optional.empty(); + return FastSeekDirection.NONE; } if (portion == DisplayPortion.LEFT // Small puffer to eliminate infinite rewind seeking && simpleExoPlayer.getCurrentPosition() > 500L) { - return Optional.of(false); + return FastSeekDirection.BACKWARD; } else if (portion == DisplayPortion.RIGHT) { - return Optional.of(true); + return FastSeekDirection.FORWARD; } /* portion == DisplayPortion.MIDDLE */ - return Optional.empty(); + return FastSeekDirection.NONE; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index 5a3eb9a4e..522977317 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -4,6 +4,7 @@ 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 @@ -13,7 +14,6 @@ import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R import org.schabi.newpipe.player.event.DisplayPortion import org.schabi.newpipe.player.event.DoubleTapListener -import java.util.Optional class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs), DoubleTapListener { @@ -64,7 +64,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : override fun onDoubleTapProgressDown(portion: DisplayPortion) { val shouldForward: Boolean = - performListener?.shouldFastForward(portion)?.orElse(null) ?: return + performListener?.getFastSeekDirection(portion)?.directionAsBoolean ?: return if (DEBUG) Log.d( @@ -125,12 +125,22 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : interface PerformListener { fun onDoubleTap() fun onDoubleTapEnd() - fun shouldFastForward(portion: DisplayPortion): Optional + /** + * 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 = "PlayerSeekOverlay" + private const val TAG = "PlayerFastSeekOverlay" private val DEBUG = MainActivity.DEBUG } } From 30ce906f724f6508dbe976d337690bc880e73b2f Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 21 Jan 2022 21:25:25 +0100 Subject: [PATCH 19/21] Apply seek conditions based on direction * When rewinding: Check if <0,5s * When fast-forwarding: Check if player has completed or the current playback has ended This allows rewinding on the endscreen --- .../org/schabi/newpipe/player/Player.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 431b4bb98..c7bc3db3a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -603,17 +603,25 @@ public final class Player implements public FastSeekDirection getFastSeekDirection( @NonNull final DisplayPortion portion ) { - // Null indicates an invalid area or condition e.g. the middle portion - // or video start or end was reached during double tap seeking - if (invalidSeekConditions()) { + if (exoPlayerIsNull()) { + // Abort seeking playerGestureListener.endMultiDoubleTap(); return FastSeekDirection.NONE; } - if (portion == DisplayPortion.LEFT - // Small puffer to eliminate infinite rewind seeking - && simpleExoPlayer.getCurrentPosition() > 500L) { + if (portion == DisplayPortion.LEFT) { + // Check if we can 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 the can fast-forward + if (currentState == STATE_COMPLETED + || simpleExoPlayer.getCurrentPosition() + >= simpleExoPlayer.getDuration()) { + return FastSeekDirection.NONE; + } return FastSeekDirection.FORWARD; } /* portion == DisplayPortion.MIDDLE */ @@ -629,14 +637,6 @@ public final class Player implements fastRewind(); } } - - private boolean invalidSeekConditions() { - return exoPlayerIsNull() - || simpleExoPlayer.getPlaybackState() - == com.google.android.exoplayer2.Player.STATE_ENDED - || simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration() - || currentState == STATE_COMPLETED; - } }); playerGestureListener.doubleTapControls(binding.fastSeekOverlay); } From 54ef604569ea723dbabf588670b27489e2b8fc98 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 21 Jan 2022 21:28:24 +0100 Subject: [PATCH 20/21] Improved docs --- app/src/main/java/org/schabi/newpipe/player/Player.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index c7bc3db3a..029b33b7f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -609,14 +609,14 @@ public final class Player implements return FastSeekDirection.NONE; } if (portion == DisplayPortion.LEFT) { - // Check if we can rewind + // 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 the can fast-forward + // Check if it's possible to fast-forward if (currentState == STATE_COMPLETED || simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { From af79479716dc387b5f138757f7015e5caecb2145 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 25 Jan 2022 20:44:49 +0100 Subject: [PATCH 21/21] Fixed "Changing the seeks duration does not update the displayed seconds" --- app/src/main/java/org/schabi/newpipe/player/Player.java | 3 ++- .../schabi/newpipe/views/player/PlayerFastSeekOverlay.kt | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 029b33b7f..b16bd8b0e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -586,7 +586,8 @@ public final class Player implements */ private void setupPlayerSeekOverlay() { binding.fastSeekOverlay - .seekSeconds((int) (retrieveSeekDurationFromPreferences(this) / 1000.0f)) + .seekSecondsSupplier( + () -> (int) (retrieveSeekDurationFromPreferences(this) / 1000.0f)) .performListener(new PlayerFastSeekOverlay.PerformListener() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index 522977317..649b60494 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -42,11 +42,10 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : performListener = listener } - var seekSeconds: Int = 0 - private set + private var seekSecondsSupplier: () -> Int = { 0 } - fun seekSeconds(seconds: Int) = apply { - seekSeconds = seconds + fun seekSecondsSupplier(supplier: () -> Int) = apply { + seekSecondsSupplier = supplier } // Indicates whether this (double) tap is the first of a series @@ -94,7 +93,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : performListener?.onDoubleTap() - secondsView.seconds += seekSeconds + secondsView.seconds += seekSecondsSupplier.invoke() performListener?.seek(forward = shouldForward) }