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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,11 +54,19 @@
tools:ignore="ContentDescription"
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
android:id="@+id/playbackControlRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/video_overlay_color"
android:fitsSystemWindows="true"
android:visibility="gone"
tools:visibility="visible">
@ -754,4 +762,11 @@
android:textColor="@color/white"
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>

View File

@ -54,11 +54,19 @@
tools:ignore="ContentDescription"
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
android:id="@+id/playbackControlRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/video_overlay_color"
android:fitsSystemWindows="true"
android:visibility="gone"
tools:visibility="visible">
@ -751,4 +759,11 @@
android:textColor="@color/white"
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>