From 72eb3b441596f93f7f1f14bdc7e98c296d4ef726 Mon Sep 17 00:00:00 2001 From: vkay94 Date: Mon, 18 Jan 2021 19:56:08 +0100 Subject: [PATCH] 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" /> + +