SeekOverlay: Add seek overlay logic to player

This commit is contained in:
vkay94 2021-01-18 19:56:08 +01:00 committed by litetex
parent 3a40759cd2
commit 72eb3b4415
8 changed files with 206 additions and 57 deletions

View file

@ -54,7 +54,7 @@ fun View.animate(
) )
Log.d(TAG, "animate(): $msg") Log.d(TAG, "animate(): $msg")
} }
if (isVisible && enterOrExit) { if (isVisible && enterOrExit && alpha == 1f && animationType == AnimationType.ALPHA) {
if (MainActivity.DEBUG) { if (MainActivity.DEBUG) {
Log.d(TAG, "animate(): view was already visible > view = [$this]") Log.d(TAG, "animate(): view was already visible > view = [$this]")
} }
@ -75,8 +75,15 @@ fun View.animate(
} }
animate().setListener(null).cancel() animate().setListener(null).cancel()
isVisible = true isVisible = true
val alphaRelativeDuration = if (enterOrExit && alpha < 1.0f) {
(duration * (1 - alpha)).toLong()
} else {
(duration * alpha).toLong()
}
when (animationType) { 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.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.LIGHT_SCALE_AND_ALPHA -> animateLightScaleAndAlpha(enterOrExit, duration, delay, execOnEnd) AnimationType.LIGHT_SCALE_AND_ALPHA -> animateLightScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.SLIDE_AND_ALPHA -> animateSlideAndAlpha(enterOrExit, duration, delay, execOnEnd) AnimationType.SLIDE_AND_ALPHA -> animateSlideAndAlpha(enterOrExit, duration, delay, execOnEnd)

View file

@ -136,6 +136,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target; import com.squareup.picasso.Target;
import org.jetbrains.annotations.NotNull;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; 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.ktx.AnimationType;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.event.DisplayPortion;
import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.event.PlayerGestureListener; import org.schabi.newpipe.player.event.PlayerGestureListener;
import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener;
@ -188,6 +190,8 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView; import org.schabi.newpipe.views.ExpandableSurfaceView;
import org.schabi.newpipe.views.player.CircleClipTapView;
import org.schabi.newpipe.views.player.PlayerSeekOverlay;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -365,6 +369,7 @@ public final class Player implements
private int maxGestureLength; // scaled private int maxGestureLength; // scaled
private GestureDetectorCompat gestureDetector; private GestureDetectorCompat gestureDetector;
private PlayerGestureListener playerGestureListener;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Listeners and disposables // Listeners and disposables
@ -525,9 +530,10 @@ public final class Player implements
binding.resizeTextView.setOnClickListener(this); binding.resizeTextView.setOnClickListener(this);
binding.playbackLiveSync.setOnClickListener(this); binding.playbackLiveSync.setOnClickListener(this);
final PlayerGestureListener listener = new PlayerGestureListener(this, service); playerGestureListener = new PlayerGestureListener(this, service);
gestureDetector = new GestureDetectorCompat(context, listener); gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
binding.getRoot().setOnTouchListener(listener); binding.getRoot().setOnTouchListener(playerGestureListener);
setupPlayerSeekOverlay();
binding.queueButton.setOnClickListener(this); binding.queueButton.setOnClickListener(this);
binding.segmentsButton.setOnClickListener(this); binding.segmentsButton.setOnClickListener(this);
@ -578,6 +584,83 @@ public final class Player implements
v.getPaddingRight(), v.getPaddingRight(),
v.getPaddingBottom())); 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 //endregion
@ -1905,6 +1988,7 @@ public final class Player implements
} }
private void showHideShadow(final boolean show, final long duration) { private void showHideShadow(final boolean show, final long duration) {
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null); animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null); animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
} }
@ -2179,18 +2263,21 @@ public final class Player implements
stopProgressLoop(); stopProgressLoop();
} }
showControls(400); // Don't let UI elements popup during double tap seeking. This state is entered sometimes
binding.loadingPanel.setVisibility(View.GONE); // during seeking/loading. This if-else check ensures that the controls aren't popping up.
if (!playerGestureListener.isDoubleTapping()) {
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0, showControls(400);
() -> { binding.loadingPanel.setVisibility(View.GONE);
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
animatePlayButtons(true, 200);
if (!isQueueVisible) {
binding.playPauseButton.requestFocus();
}
});
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
() -> {
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
animatePlayButtons(true, 200);
if (!isQueueVisible) {
binding.playPauseButton.requestFocus();
}
});
}
changePopupWindowFlags(IDLE_WINDOW_FLAGS); changePopupWindowFlags(IDLE_WINDOW_FLAGS);
// Remove running notification when user does not want minimization to background or popup // Remove running notification when user does not want minimization to background or popup
@ -2838,7 +2925,6 @@ public final class Player implements
} }
seekBy(retrieveSeekDurationFromPreferences(this)); seekBy(retrieveSeekDurationFromPreferences(this));
triggerProgressUpdate(); triggerProgressUpdate();
showAndAnimateControl(R.drawable.ic_fast_forward, true);
} }
public void fastRewind() { public void fastRewind() {
@ -2847,7 +2933,6 @@ public final class Player implements
} }
seekBy(-retrieveSeekDurationFromPreferences(this)); seekBy(-retrieveSeekDurationFromPreferences(this));
triggerProgressUpdate(); triggerProgressUpdate();
showAndAnimateControl(R.drawable.ic_fast_rewind, true);
} }
//endregion //endregion
@ -4279,6 +4364,10 @@ public final class Player implements
return binding.currentDisplaySeek; return binding.currentDisplaySeek;
} }
public PlayerSeekOverlay getSeekOverlay() {
return binding.seekOverlay;
}
@Nullable @Nullable
public WindowManager.LayoutParams getPopupLayoutParams() { public WindowManager.LayoutParams getPopupLayoutParams() {
return popupLayoutParams; return popupLayoutParams;

View file

@ -55,12 +55,10 @@ public class PlayerGestureListener
player.hideControls(0, 0); player.hideControls(0, 0);
} }
if (portion == DisplayPortion.LEFT) { if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
player.fastRewind(); startMultiDoubleTap(event);
} else if (portion == DisplayPortion.MIDDLE) { } else if (portion == DisplayPortion.MIDDLE) {
player.playPause(); player.playPause();
} else if (portion == DisplayPortion.RIGHT) {
player.fastForward();
} }
} }
@ -236,6 +234,7 @@ public class PlayerGestureListener
player.getLoadingPanel().setVisibility(View.GONE); player.getLoadingPanel().setVisibility(View.GONE);
player.hideControls(0, 0); player.hideControls(0, 0);
animate(player.getSeekOverlay(), false, 0);
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0); animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
} }

View file

@ -11,7 +11,7 @@ import org.schabi.newpipe.player.event.DisplayPortion
class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, attrs) { class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, attrs) {
companion object { companion object {
const val COLOR_DARK = 0x40000000 const val COLOR_DARK = 0x45000000
const val COLOR_DARK_TRANSPARENT = 0x30000000 const val COLOR_DARK_TRANSPARENT = 0x30000000
const val COLOR_LIGHT_TRANSPARENT = 0x25EEEEEE const val COLOR_LIGHT_TRANSPARENT = 0x25EEEEEE

View file

@ -5,24 +5,30 @@ import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View 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
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.constraintlayout.widget.ConstraintSet.*
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.player_seek_overlay.view.*
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.player.event.DisplayPortion import org.schabi.newpipe.player.event.DisplayPortion
import org.schabi.newpipe.player.event.DoubleTapListener 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 { ConstraintLayout(context, attrs), DoubleTapListener {
private var secondsView: SecondsView private var secondsView: SecondsView
private var circleClipTapView: CircleClipTapView private var circleClipTapView: CircleClipTapView
private var rootConstraintLayout: ConstraintLayout
private var isForwarding: Boolean? = null private var isForwarding: Boolean? = null
@ -31,6 +37,7 @@ class PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) :
secondsView = findViewById(R.id.seconds_view) secondsView = findViewById(R.id.seconds_view)
circleClipTapView = findViewById(R.id.circle_clip_tap_view) circleClipTapView = findViewById(R.id.circle_clip_tap_view)
rootConstraintLayout = findViewById(R.id.root_constraint_layout)
initializeAttributes() initializeAttributes()
secondsView.isForward = true secondsView.isForward = true
@ -161,11 +168,14 @@ class PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) :
val shouldForward: Boolean = performListener?.shouldFastForward(portion) ?: return val shouldForward: Boolean = performListener?.shouldFastForward(portion) ?: return
if (DEBUG) if (DEBUG)
Log.d(TAG,"onDoubleTapProgressDown called with " + Log.d(
"shouldForward = [$shouldForward], " + TAG,
"isForwarding = [$isForwarding], " + "onDoubleTapProgressDown called with " +
"secondsView#isForward = [${secondsView.isForward}], " + "shouldForward = [$shouldForward], " +
"initTap = [$initTap], ") "isForwarding = [$isForwarding], " +
"secondsView#isForward = [${secondsView.isForward}], " +
"initTap = [$initTap], "
)
// Using this check prevents from fast switching (one touches) // Using this check prevents from fast switching (one touches)
if (isForwarding != null && isForwarding != shouldForward) { if (isForwarding != null && isForwarding != shouldForward) {
@ -234,18 +244,22 @@ class PlayerSeekOverlay(context: Context?, private val attrs: AttributeSet?) :
private fun changeConstraints(forward: Boolean) { private fun changeConstraints(forward: Boolean) {
val constraintSet = ConstraintSet() val constraintSet = ConstraintSet()
with(constraintSet) { with(constraintSet) {
clone(root_constraint_layout) clone(rootConstraintLayout)
if (forward) { if (forward) {
clear(seconds_view.id, START) clear(secondsView.id, START)
connect(seconds_view.id, END, connect(
PARENT_ID, END) secondsView.id, END,
PARENT_ID, END
)
} else { } else {
clear(seconds_view.id, END) clear(secondsView.id, END)
connect(seconds_view.id, START, connect(
PARENT_ID, START) secondsView.id, START,
PARENT_ID, START
)
} }
secondsView.start() secondsView.start()
applyTo(root_constraint_layout) applyTo(rootConstraintLayout)
} }
} }

View file

@ -11,7 +11,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import kotlinx.android.synthetic.main.player_seek_seconds_view.view.* import kotlinx.android.synthetic.main.player_seek_seconds_view.view.*
import org.schabi.newpipe.R import org.schabi.newpipe.R
class SecondsView(context: Context?, attrs: AttributeSet?) : class SecondsView(context: Context, attrs: AttributeSet?) :
ConstraintLayout(context, attrs) { ConstraintLayout(context, attrs) {
companion object { companion object {
@ -45,7 +45,6 @@ class SecondsView(context: Context?, attrs: AttributeSet?) :
val textView: TextView val textView: TextView
get() = tv_seconds get() = tv_seconds
@DrawableRes @DrawableRes
var icon: Int = R.drawable.ic_play_seek_triangle var icon: Int = R.drawable.ic_play_seek_triangle
set(value) { set(value) {
@ -87,9 +86,11 @@ class SecondsView(context: Context?, attrs: AttributeSet?) :
icon_1.alpha = 0f icon_1.alpha = 0f
icon_2.alpha = 0f icon_2.alpha = 0f
icon_3.alpha = 0f icon_3.alpha = 0f
}, { },
{
icon_1.alpha = it icon_1.alpha = it
}, { },
{
secondAnimator.start() secondAnimator.start()
} }
) )
@ -99,9 +100,11 @@ class SecondsView(context: Context?, attrs: AttributeSet?) :
icon_1.alpha = 1f icon_1.alpha = 1f
icon_2.alpha = 0f icon_2.alpha = 0f
icon_3.alpha = 0f icon_3.alpha = 0f
}, { },
{
icon_2.alpha = it icon_2.alpha = it
}, { },
{
thirdAnimator.start() thirdAnimator.start()
} }
) )
@ -111,10 +114,12 @@ class SecondsView(context: Context?, attrs: AttributeSet?) :
icon_1.alpha = 1f icon_1.alpha = 1f
icon_2.alpha = 1f icon_2.alpha = 1f
icon_3.alpha = 0f icon_3.alpha = 0f
}, { },
{
icon_1.alpha = 1f - icon_3.alpha icon_1.alpha = 1f - icon_3.alpha
icon_3.alpha = it icon_3.alpha = it
}, { },
{
fourthAnimator.start() fourthAnimator.start()
} }
) )
@ -124,9 +129,11 @@ class SecondsView(context: Context?, attrs: AttributeSet?) :
icon_1.alpha = 0f icon_1.alpha = 0f
icon_2.alpha = 1f icon_2.alpha = 1f
icon_3.alpha = 1f icon_3.alpha = 1f
}, { },
{
icon_2.alpha = 1f - it icon_2.alpha = 1f - it
}, { },
{
fifthAnimator.start() fifthAnimator.start()
} }
) )
@ -136,16 +143,20 @@ class SecondsView(context: Context?, attrs: AttributeSet?) :
icon_1.alpha = 0f icon_1.alpha = 0f
icon_2.alpha = 0f icon_2.alpha = 0f
icon_3.alpha = 1f icon_3.alpha = 1f
}, { },
{
icon_3.alpha = 1f - it icon_3.alpha = 1f - it
}, { },
{
firstAnimator.start() firstAnimator.start()
} }
) )
private inner class CustomValueAnimator( private inner class CustomValueAnimator(
start: () -> Unit, update: (value: Float) -> Unit, end: () -> Unit start: () -> Unit,
): ValueAnimator() { update: (value: Float) -> Unit,
end: () -> Unit
) : ValueAnimator() {
init { init {
duration = cycleDuration / 5 duration = cycleDuration / 5
@ -164,7 +175,6 @@ class SecondsView(context: Context?, attrs: AttributeSet?) :
override fun onAnimationCancel(animation: Animator?) = Unit override fun onAnimationCancel(animation: Animator?) = Unit
override fun onAnimationRepeat(animation: Animator?) = Unit override fun onAnimationRepeat(animation: Animator?) = Unit
}) })
} }
} }

View file

@ -54,11 +54,19 @@
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:visibility="visible" /> tools:visibility="visible" />
<View
android:id="@+id/playbackControlsShadow"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:layout_alignBottom="@+id/playbackControlRoot"
android:background="@color/video_overlay_color"
tools:visibility="visible" />
<RelativeLayout <RelativeLayout
android:id="@+id/playbackControlRoot" android:id="@+id/playbackControlRoot"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/video_overlay_color"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
@ -754,4 +762,11 @@
android:textColor="@color/white" android:textColor="@color/white"
android:visibility="gone" /> android:visibility="gone" />
<org.schabi.newpipe.views.player.PlayerSeekOverlay
android:id="@+id/seekOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
android:alpha="0" /> <!-- Required to make the first appearance fading correctly -->
</RelativeLayout> </RelativeLayout>

View file

@ -54,11 +54,19 @@
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:visibility="visible" /> tools:visibility="visible" />
<View
android:id="@+id/playbackControlsShadow"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:layout_alignBottom="@+id/playbackControlRoot"
android:background="@color/video_overlay_color"
tools:visibility="visible" />
<RelativeLayout <RelativeLayout
android:id="@+id/playbackControlRoot" android:id="@+id/playbackControlRoot"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/video_overlay_color"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
@ -751,4 +759,11 @@
android:textColor="@color/white" android:textColor="@color/white"
android:visibility="gone" /> android:visibility="gone" />
<org.schabi.newpipe.views.player.PlayerSeekOverlay
android:id="@+id/seekOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
android:alpha="0" /> <!-- Required to make the first appearance fading corectly -->
</RelativeLayout> </RelativeLayout>