feat: Add hold-to-fast-forward (2x speed) feature

- Add tap and hold gesture to increase playback speed to 2x while held
- Add settings toggle in Video & Audio settings to enable/disable the feature
- Add visual indicator overlay showing '2x Speed' when feature is active
- Feature is enabled by default and respects user preference
This commit is contained in:
Carl Miller 2025-11-27 22:45:56 -06:00
parent 1bca5f3d63
commit 6eba36700a
8 changed files with 168 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import android.view.View
import androidx.core.os.postDelayed
import org.schabi.newpipe.databinding.PlayerBinding
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.VideoPlayerUi
/**
@ -24,11 +25,87 @@ abstract class BasePlayerGestureListener(
protected val player: Player = playerUi.player
protected val binding: PlayerBinding = playerUi.binding
// ///////////////////////////////////////////////////////////////////
// Hold to fast forward (2x speed)
// ///////////////////////////////////////////////////////////////////
private var isHoldingForFastForward = false
private var originalPlaybackSpeed = 1.0f
private val fastForwardSpeed = 2.0f
override fun onTouch(v: View, event: MotionEvent): Boolean {
playerUi.gestureDetector.onTouchEvent(event)
// Handle touch up to restore original speed when hold-to-fast-forward is active
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
if (isHoldingForFastForward) {
stopHoldToFastForward()
}
}
return false
}
override fun onLongPress(e: MotionEvent) {
if (DEBUG) {
Log.d(TAG, "onLongPress called with e = [$e]")
}
// Check if hold-to-fast-forward is enabled in settings
if (!PlayerHelper.isHoldToFastForwardEnabled(player.context)) {
return
}
// Only activate if player is playing and not in a popup menu
if (player.currentState != Player.STATE_PLAYING || playerUi.isSomePopupMenuVisible) {
return
}
// Don't activate during double tap mode
if (isDoubleTapping) {
return
}
startHoldToFastForward()
}
private fun startHoldToFastForward() {
if (isHoldingForFastForward) {
return
}
if (DEBUG) {
Log.d(TAG, "startHoldToFastForward: activating 2x speed")
}
isHoldingForFastForward = true
originalPlaybackSpeed = player.playbackSpeed
// Set playback speed to 2x
player.setPlaybackSpeed(fastForwardSpeed)
// Show visual feedback
playerUi.onHoldToFastForwardStart()
}
private fun stopHoldToFastForward() {
if (!isHoldingForFastForward) {
return
}
if (DEBUG) {
Log.d(TAG, "stopHoldToFastForward: restoring original speed $originalPlaybackSpeed")
}
isHoldingForFastForward = false
// Restore original playback speed
player.setPlaybackSpeed(originalPlaybackSpeed)
// Hide visual feedback
playerUi.onHoldToFastForwardEnd()
}
private fun onDoubleTap(
event: MotionEvent,
portion: DisplayPortion

View File

@ -231,6 +231,11 @@ public final class PlayerHelper {
.getBoolean(context.getString(R.string.auto_queue_key), false);
}
public static boolean isHoldToFastForwardEnabled(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.hold_to_fast_forward_key), true);
}
public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.clear_queue_confirmation_key), false);

View File

@ -441,6 +441,33 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Hold to fast forward
//////////////////////////////////////////////////////////////////////////*/
//region Hold to fast forward
/**
* Called when hold-to-fast-forward is activated (long press detected).
* Shows the visual indicator overlay.
*/
public void onHoldToFastForwardStart() {
animate(binding.holdToFastForwardOverlay, true, DEFAULT_CONTROLS_DURATION);
// Hide controls while fast forwarding
if (isControlsVisible()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0);
}
}
/**
* Called when hold-to-fast-forward is deactivated (finger released).
* Hides the visual indicator overlay.
*/
public void onHoldToFastForwardEnd() {
animate(binding.holdToFastForwardOverlay, false, DEFAULT_CONTROLS_DURATION);
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/defaultIconTint"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z" />
</vector>

View File

@ -809,4 +809,40 @@
android:alpha="0"
android:visibility="invisible" /> <!-- Required for the first appearance fading correctly -->
<!-- Hold to fast forward indicator -->
<LinearLayout
android:id="@+id/holdToFastForwardOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="80dp"
android:background="@drawable/background_oval_black_transparent"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="12dp"
android:paddingEnd="20dp"
android:paddingBottom="12dp"
android:visibility="gone"
tools:visibility="visible">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_fast_forward"
app:tint="@color/white"
tools:ignore="ContentDescription" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/holdToFastForwardText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hold_to_fast_forward_indicator"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</RelativeLayout>

View File

@ -25,6 +25,8 @@
<string name="clear_queue_confirmation_key">clear_queue_confirmation_key</string>
<string name="ignore_hardware_media_buttons_key">ignore_hardware_media_buttons_key</string>
<string name="hold_to_fast_forward_key">hold_to_fast_forward_key</string>
<string name="popup_saved_width_key">popup_saved_width</string>
<string name="popup_saved_x_key">popup_saved_x</string>
<string name="popup_saved_y_key">popup_saved_y</string>

View File

@ -91,6 +91,9 @@
<string name="clear_queue_confirmation_description">The active player queue will be replaced</string>
<string name="ignore_hardware_media_buttons_title">Ignore hardware media button events</string>
<string name="ignore_hardware_media_buttons_summary">Useful, for instance, if you are using a headset with broken physical buttons</string>
<string name="hold_to_fast_forward_title">Hold to fast forward</string>
<string name="hold_to_fast_forward_summary">Tap and hold on the video to play at 2x speed while held</string>
<string name="hold_to_fast_forward_indicator">2x Speed</string>
<string name="show_comments_title">Show comments</string>
<string name="show_comments_summary">Turn off to hide comments</string>
<string name="show_next_and_similar_title">Show \'Next\' and \'Similar\' videos</string>

View File

@ -249,5 +249,13 @@
android:title="@string/ignore_hardware_media_buttons_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="@string/hold_to_fast_forward_key"
android:summary="@string/hold_to_fast_forward_summary"
android:title="@string/hold_to_fast_forward_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceScreen>