SeekOverlay: Add seek overlay logic to player
This commit is contained in:
parent
3a40759cd2
commit
72eb3b4415
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue