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 a11f1f64e..74a208dc5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -5493,6 +5493,10 @@ public TextView getSwipeSeekDisplay() {
return binding.swipeSeekDisplay;
}
+ public TextView getSwipeSpeedDisplay() {
+ return binding.swipeSpeedDisplay;
+ }
+
public PlayerFastSeekOverlay getFastSeekOverlay() {
return binding.fastSeekOverlay;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt
index 081d640dd..cc18f9afd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt
@@ -101,7 +101,11 @@ abstract class BasePlayerGestureListener(
// Check if swiping up (negative y velocity)
if (yVelocity < 0) {
- v.parent.requestDisallowInterceptTouchEvent(player.isFullscreenGestureEnabled || player.isFullscreen)
+ v.parent.requestDisallowInterceptTouchEvent(
+ player.isFullscreenGestureEnabled
+ || player.isFullscreen
+ || PlayerHelper.isPlaybackSpeedGestureEnabled(service)
+ )
} else {
v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
}
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 b46b7fb4f..e67aa1104 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
@@ -7,6 +7,8 @@
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
+import java.util.Locale;
+
import android.app.Activity;
import android.util.Log;
import android.view.MotionEvent;
@@ -49,6 +51,12 @@ public class PlayerGestureListener
private long swipeSeekTargetPosition = 0L;
private boolean isChangingVolume = false;
private boolean isChangingBrightness = false;
+ private boolean isChangingSpeed = false;
+ private float speedGestureStartSpeed = 1.0f;
+ private float accumulatedSpeedScroll = 0f;
+ private static final float SPEED_SWIPE_PIXELS_PER_STEP = 20f;
+ private static final float SPEED_MIN = 0.1f;
+ private static final float SPEED_MAX = 4.0f;
private boolean isPendingScreenRotation = false;
private boolean isFullscreenRotationGesture = false;
@@ -127,6 +135,11 @@ public void onScroll(@NonNull final PlayerService.PlayerType playerType,
return;
}
+ if (isChangingSpeed) {
+ onScrollMainSpeed(distanceY);
+ return;
+ }
+
final boolean isHorizontal = Math.abs(distanceX) > Math.abs(distanceY);
if (!isHorizontal && isFullscreenGestureEnabled &&
((player.isFullscreen() && distanceY < 0 && portion == DisplayPortion.MIDDLE) ||
@@ -139,6 +152,15 @@ public void onScroll(@NonNull final PlayerService.PlayerType playerType,
if(!player.isFullscreen()) {
return;
}
+
+ final boolean isPlaybackSpeedGestureEnabled =
+ PlayerHelper.isPlaybackSpeedGestureEnabled(service);
+ if (!isHorizontal && isPlaybackSpeedGestureEnabled && player.isFullscreen()
+ && portion == DisplayPortion.MIDDLE) {
+ onScrollMainSpeed(distanceY);
+ return;
+ }
+
if (isSwipeSeekGestureEnabled && isHorizontal) {
onScrollMainSeek(distanceX);
return;
@@ -258,6 +280,31 @@ private void onScrollMainBrightness(final float distanceX, final float distanceY
}
}
+ private void onScrollMainSpeed(final float distanceY) {
+ if (!isChangingSpeed) {
+ isChangingSpeed = true;
+ accumulatedSpeedScroll = 0f;
+ speedGestureStartSpeed = player.getPlaybackSpeed();
+ animate(player.getSwipeSpeedDisplay(), true, DEFAULT_CONTROLS_DURATION, SCALE_AND_ALPHA);
+ if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
+ animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA);
+ isChangingVolume = false;
+ }
+ if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
+ animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA);
+ isChangingBrightness = false;
+ }
+ }
+
+ accumulatedSpeedScroll += distanceY; // positive = swipe up = faster
+ final float rawSpeed = speedGestureStartSpeed
+ + (accumulatedSpeedScroll / SPEED_SWIPE_PIXELS_PER_STEP) * 0.1f;
+ final float speed = Math.max(SPEED_MIN, Math.min(SPEED_MAX,
+ Math.round(rawSpeed * 10) / 10.0f));
+ player.setPlaybackSpeed(speed);
+ player.getSwipeSpeedDisplay().setText(String.format(Locale.getDefault(), "%.1fx", speed));
+ }
+
private void onScrollMainSeek(final float distanceX) {
// The first swipe determines the active overlay; once seeking is engaged
// we hide volume and brightness controls so mixed movements do not trigger them.
@@ -327,6 +374,10 @@ public void onScrollEnd(@NonNull final PlayerService.PlayerType playerType,
animate(player.getSwipeSeekDisplay(), false, 200, SCALE_AND_ALPHA);
isSwipeSeeking = false;
}
+ if (isChangingSpeed) {
+ animate(player.getSwipeSpeedDisplay(), false, 200, SCALE_AND_ALPHA, 200);
+ isChangingSpeed = false;
+ }
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
@@ -357,10 +408,12 @@ public void onPopupResizingStart() {
player.hideControls(0, 0);
animate(player.getFastSeekOverlay(), false, 0);
animate(player.getSwipeSeekDisplay(), false, 0, ALPHA, 0);
+ animate(player.getSwipeSpeedDisplay(), false, 0, ALPHA, 0);
animate(player.getVolumeRelativeLayout(), false, 0, ALPHA, 0);
animate(player.getBrightnessRelativeLayout(), false, 0, ALPHA, 0);
isChangingVolume = false;
isChangingBrightness = false;
+ isChangingSpeed = false;
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index 5aa298d03..51fbff46f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -334,6 +334,11 @@ public static boolean isSwipeSeekGestureEnabled(@NonNull final Context context)
.getBoolean(context.getString(R.string.swipe_seek_gesture_control_key), true);
}
+ public static boolean isPlaybackSpeedGestureEnabled(@NonNull final Context context) {
+ return getPreferences(context)
+ .getBoolean(context.getString(R.string.playback_speed_gesture_control_key), false);
+ }
+
public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/GestureSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/GestureSettingsFragment.java
index d117bc13b..c3b6e6ac1 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/GestureSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/GestureSettingsFragment.java
@@ -6,6 +6,7 @@
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
+import androidx.preference.SwitchPreferenceCompat;
import org.schabi.newpipe.R;
@@ -19,6 +20,32 @@ public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
addPreferencesFromResourceRegistry();
updateSeekOptions();
+ setupSpeedGestureMutualExclusion();
+ }
+
+ private void setupSpeedGestureMutualExclusion() {
+ final SwitchPreferenceCompat fullscreenPref = findPreference(
+ getString(R.string.fullscreen_gesture_control_key));
+ final SwitchPreferenceCompat speedPref = findPreference(
+ getString(R.string.playback_speed_gesture_control_key));
+
+ if (fullscreenPref == null || speedPref == null) {
+ return;
+ }
+
+ fullscreenPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (Boolean.TRUE.equals(newValue)) {
+ speedPref.setChecked(false);
+ }
+ return true;
+ });
+
+ speedPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (Boolean.TRUE.equals(newValue)) {
+ fullscreenPref.setChecked(false);
+ }
+ return true;
+ });
}
private void updateSeekOptions() {
diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml
index d2b4d022e..70a860759 100644
--- a/app/src/main/res/layout/player.xml
+++ b/app/src/main/res/layout/player.xml
@@ -920,6 +920,20 @@
tools:ignore="RtlHardcoded"
tools:text="+0:00 (0:00)" />
+
+
brightness_gesture_control
fullscreen_gesture_control
swipe_seek_gesture_control
+ playback_speed_gesture_control
resume_on_audio_focus_gain
popup_remember_size_pos_key
use_inexact_seek_key
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e111ae3c7..7a31b2c58 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -93,6 +93,8 @@
Use gestures to control player brightness
Swipe to seek gesture
Swipe right to fast-forward, and left to rewind
+Swipe to control playback speed
+Use gestures to control playback speed. Disables fullscreen gesture control
Search suggestions
Choose the suggestions to show when searching
Local search suggestions
@@ -922,7 +924,7 @@
View on GitHub
Support the Project
Thank you for using PipePipe! If you find it useful, please consider becoming a supporter on Ko-Fi. Your support is important to me and helps me add more exciting new features. Every bit counts! 😇
-Use gestures to enter / exit fullscreen
+Use gestures to enter / exit fullscreen. Disables playback speed gesture control
Fullscreen gesture control
Make sure only PipePipe is playing audio
Require audio focus
diff --git a/app/src/main/res/xml/gesture_settings.xml b/app/src/main/res/xml/gesture_settings.xml
index dece82fe9..7086453d1 100644
--- a/app/src/main/res/xml/gesture_settings.xml
+++ b/app/src/main/res/xml/gesture_settings.xml
@@ -26,6 +26,14 @@
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
+
+