Skip to content

Commit d04934d

Browse files
committed
Add player gesture features
1 parent acbe703 commit d04934d

8 files changed

Lines changed: 234 additions & 5 deletions

File tree

app/src/main/java/org/schabi/newpipe/player/Player.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,13 @@ public void setPlaybackSpeed(final float speed) {
950950
setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence());
951951
}
952952

953+
public void setPlaybackSpeedTemporarily(final float speed) {
954+
if (!exoPlayerIsNull()) {
955+
simpleExoPlayer.setPlaybackParameters(
956+
new PlaybackParameters(speed, getPlaybackPitch()));
957+
}
958+
}
959+
953960
public float getPlaybackPitch() {
954961
return getPlaybackParameters().pitch;
955962
}

app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.view.View
99
import androidx.core.os.postDelayed
1010
import org.schabi.newpipe.databinding.PlayerBinding
1111
import org.schabi.newpipe.player.Player
12+
import org.schabi.newpipe.player.helper.PlayerHelper
1213
import org.schabi.newpipe.player.ui.VideoPlayerUi
1314

1415
/**
@@ -24,8 +25,14 @@ abstract class BasePlayerGestureListener(
2425
protected val player: Player = playerUi.player
2526
protected val binding: PlayerBinding = playerUi.binding
2627

28+
private var isHoldToSpeedActive = false
29+
private var previousPlaybackSpeed = 1.0f
30+
2731
override fun onTouch(v: View, event: MotionEvent): Boolean {
2832
playerUi.gestureDetector.onTouchEvent(event)
33+
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
34+
restoreHoldToSpeed()
35+
}
2936
return false
3037
}
3138

@@ -120,6 +127,27 @@ abstract class BasePlayerGestureListener(
120127
return true
121128
}
122129

130+
override fun onLongPress(e: MotionEvent) {
131+
if (isHoldToSpeedActive || !PlayerHelper.isHoldToSpeedEnabled(player.context) ||
132+
player.exoPlayerIsNull()
133+
) {
134+
return
135+
}
136+
137+
previousPlaybackSpeed = player.playbackSpeed
138+
player.setPlaybackSpeedTemporarily(PlayerHelper.getHoldToSpeedValue(player.context))
139+
isHoldToSpeedActive = true
140+
}
141+
142+
private fun restoreHoldToSpeed() {
143+
if (!isHoldToSpeedActive) {
144+
return
145+
}
146+
147+
player.setPlaybackSpeedTemporarily(previousPlaybackSpeed)
148+
isHoldToSpeedActive = false
149+
}
150+
123151
// ///////////////////////////////////////////////////////////////////
124152
// Multi double tapping
125153
// ///////////////////////////////////////////////////////////////////

app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
99
import androidx.appcompat.content.res.AppCompatResources
1010
import androidx.core.view.isVisible
1111
import kotlin.math.abs
12+
import kotlin.math.sign
1213
import org.schabi.newpipe.MainActivity
1314
import org.schabi.newpipe.R
1415
import org.schabi.newpipe.ktx.AnimationType
@@ -17,6 +18,7 @@ import org.schabi.newpipe.player.Player
1718
import org.schabi.newpipe.player.helper.AudioReactor
1819
import org.schabi.newpipe.player.helper.PlayerHelper
1920
import org.schabi.newpipe.player.ui.MainPlayerUi
21+
import org.schabi.newpipe.util.Localization
2022
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
2123

2224
/**
@@ -31,15 +33,26 @@ class MainPlayerGestureListener(
3133
) : BasePlayerGestureListener(playerUi), OnTouchListener {
3234
private var isMoving = false
3335

36+
private var isSwipeSeeking = false
37+
private var accumulatedSeek = 0f
38+
private var swipeSeekStartPosition = 0L
39+
private var swipeSeekTargetPosition = 0L
40+
41+
private var isPendingFullscreenSwipe = false
42+
3443
override fun onTouch(v: View, event: MotionEvent): Boolean {
3544
super.onTouch(v, event)
36-
if (event.action == MotionEvent.ACTION_UP && isMoving) {
45+
if ((event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) &&
46+
isMoving
47+
) {
3748
isMoving = false
3849
onScrollEnd(event)
3950
}
4051
return when (event.action) {
4152
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
42-
v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
53+
v.parent?.requestDisallowInterceptTouchEvent(
54+
playerUi.isFullscreen || PlayerHelper.isFullscreenGestureEnabled(player.context)
55+
)
4356
true
4457
}
4558

@@ -156,6 +169,16 @@ class MainPlayerGestureListener(
156169

157170
override fun onScrollEnd(event: MotionEvent) {
158171
super.onScrollEnd(event)
172+
if (isPendingFullscreenSwipe) {
173+
playerUi.toggleFullscreen()
174+
isPendingFullscreenSwipe = false
175+
return
176+
}
177+
if (isSwipeSeeking) {
178+
player.seekTo(swipeSeekTargetPosition)
179+
binding.swipeSeekDisplay.animate(false, 200, AnimationType.SCALE_AND_ALPHA)
180+
isSwipeSeeking = false
181+
}
159182
if (binding.volumeRelativeLayout.isVisible) {
160183
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
161184
}
@@ -164,13 +187,49 @@ class MainPlayerGestureListener(
164187
}
165188
}
166189

190+
private fun onScrollSeek(distanceX: Float) {
191+
val duration = player.exoPlayer?.duration ?: 0L
192+
if (duration <= 0L) {
193+
return
194+
}
195+
196+
if (!isSwipeSeeking) {
197+
isSwipeSeeking = true
198+
accumulatedSeek = 0f
199+
swipeSeekStartPosition = player.exoPlayer?.currentPosition ?: 0L
200+
swipeSeekTargetPosition = swipeSeekStartPosition
201+
binding.swipeSeekDisplay.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
202+
binding.volumeRelativeLayout.isVisible = false
203+
binding.brightnessRelativeLayout.isVisible = false
204+
}
205+
206+
accumulatedSeek -= distanceX
207+
val thresholdPx = SEEK_SWIPE_FAST_THRESHOLD_MS / SEEK_SWIPE_FACTOR
208+
val deltaMs = if (abs(accumulatedSeek) <= thresholdPx) {
209+
(accumulatedSeek * SEEK_SWIPE_FACTOR).toLong()
210+
} else {
211+
val beyond = abs(accumulatedSeek) - thresholdPx
212+
(sign(accumulatedSeek) *
213+
(SEEK_SWIPE_FAST_THRESHOLD_MS +
214+
beyond * SEEK_SWIPE_FACTOR * SEEK_SWIPE_FAST_MULTIPLIER)).toLong()
215+
}
216+
217+
swipeSeekTargetPosition = (swipeSeekStartPosition + deltaMs).coerceIn(0L, duration)
218+
219+
val delta = swipeSeekTargetPosition - swipeSeekStartPosition
220+
val deltaText = (if (delta >= 0) "+" else "-") +
221+
Localization.getDurationString(abs(delta) / 1000L)
222+
val targetText = Localization.getDurationString(swipeSeekTargetPosition / 1000L)
223+
binding.swipeSeekDisplay.text = "$deltaText ($targetText)"
224+
}
225+
167226
override fun onScroll(
168227
initialEvent: MotionEvent?,
169228
movingEvent: MotionEvent,
170229
distanceX: Float,
171230
distanceY: Float
172231
): Boolean {
173-
if (initialEvent == null || !playerUi.isFullscreen) {
232+
if (initialEvent == null) {
174233
return false
175234
}
176235

@@ -185,16 +244,42 @@ class MainPlayerGestureListener(
185244
return false
186245
}
187246

188-
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
247+
val isHorizontal = abs(distanceX) > abs(distanceY)
248+
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD &&
249+
abs(movingEvent.x - initialEvent.x) <= MOVEMENT_THRESHOLD
189250
if (
190-
!isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
251+
!isMoving && insideThreshold ||
191252
player.currentState == Player.STATE_COMPLETED
192253
) {
193254
return false
194255
}
195256

196257
isMoving = true
197258

259+
if (isSwipeSeeking) {
260+
onScrollSeek(distanceX)
261+
return true
262+
}
263+
264+
if (PlayerHelper.isFullscreenGestureEnabled(player.context) && !isHorizontal) {
265+
val portion = getDisplayPortion(initialEvent)
266+
if ((playerUi.isFullscreen && distanceY < 0 && portion == DisplayPortion.MIDDLE) ||
267+
(!playerUi.isFullscreen && distanceY > 0)
268+
) {
269+
isPendingFullscreenSwipe = true
270+
return true
271+
}
272+
}
273+
274+
if (!playerUi.isFullscreen) {
275+
return false
276+
}
277+
278+
if (PlayerHelper.isSwipeSeekGestureEnabled(player.context) && isHorizontal) {
279+
onScrollSeek(distanceX)
280+
return true
281+
}
282+
198283
// -- Brightness and Volume control --
199284
if (getDisplayHalfPortion(initialEvent) == DisplayPortion.RIGHT_HALF) {
200285
when (PlayerHelper.getActionForRightGestureSide(player.context)) {
@@ -236,5 +321,8 @@ class MainPlayerGestureListener(
236321
private val TAG = MainPlayerGestureListener::class.java.simpleName
237322
private val DEBUG = MainActivity.DEBUG
238323
private const val MOVEMENT_THRESHOLD = 40
324+
private const val SEEK_SWIPE_FACTOR = 100f // ms per pixel
325+
private const val SEEK_SWIPE_FAST_MULTIPLIER = 10f
326+
private const val SEEK_SWIPE_FAST_THRESHOLD_MS = 60_000L
239327
}
240328
}

app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,37 @@ public static String getActionForLeftGestureSide(@NonNull final Context context)
220220
context.getString(R.string.default_left_gesture_control_value));
221221
}
222222

223+
public static boolean isSwipeSeekGestureEnabled(@NonNull final Context context) {
224+
return getPreferences(context)
225+
.getBoolean(context.getString(R.string.swipe_seek_gesture_control_key), true);
226+
}
227+
228+
public static boolean isFullscreenGestureEnabled(@NonNull final Context context) {
229+
return getPreferences(context)
230+
.getBoolean(context.getString(R.string.fullscreen_gesture_control_key), true);
231+
}
232+
233+
public static boolean isHoldToSpeedEnabled(@NonNull final Context context) {
234+
return !context.getString(R.string.hold_to_speed_off_value)
235+
.equals(getPreferences(context).getString(
236+
context.getString(R.string.hold_to_speed_key),
237+
context.getString(R.string.hold_to_speed_default_value)));
238+
}
239+
240+
public static float getHoldToSpeedValue(@NonNull final Context context) {
241+
final String value = getPreferences(context).getString(
242+
context.getString(R.string.hold_to_speed_key),
243+
context.getString(R.string.hold_to_speed_default_value));
244+
if (context.getString(R.string.hold_to_speed_off_value).equals(value)) {
245+
return 1.0f;
246+
}
247+
try {
248+
return Float.parseFloat(value);
249+
} catch (final NullPointerException | NumberFormatException e) {
250+
return Float.parseFloat(context.getString(R.string.hold_to_speed_default_value));
251+
}
252+
}
253+
223254
public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) {
224255
return getPreferences(context)
225256
.getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);

app/src/main/res/layout/player.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,23 @@
776776
tools:src="@drawable/ic_brightness_high" />
777777
</RelativeLayout>
778778

779+
<org.schabi.newpipe.views.NewPipeTextView
780+
android:id="@+id/swipeSeekDisplay"
781+
android:layout_width="wrap_content"
782+
android:layout_height="wrap_content"
783+
android:layout_centerInParent="true"
784+
android:background="@drawable/background_oval_black_transparent"
785+
android:paddingStart="18dp"
786+
android:paddingTop="12dp"
787+
android:paddingEnd="18dp"
788+
android:paddingBottom="12dp"
789+
android:textColor="@android:color/white"
790+
android:textSize="20sp"
791+
android:textStyle="bold"
792+
android:visibility="gone"
793+
tools:text="+00:15 (03:12)"
794+
tools:visibility="visible" />
795+
779796
</RelativeLayout>
780797

781798
</RelativeLayout>

app/src/main/res/values/settings_keys.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,30 @@
232232
<item>@string/none_control_key</item>
233233
</string-array>
234234

235+
<string name="swipe_seek_gesture_control_key">swipe_seek_gesture_control</string>
236+
<string name="fullscreen_gesture_control_key">fullscreen_gesture_control</string>
237+
<string name="hold_to_speed_key">hold_to_speed</string>
238+
<string name="hold_to_speed_default_value">2.0</string>
239+
<string name="hold_to_speed_off_value">off</string>
240+
<string-array name="hold_to_speed_entries">
241+
<item>@string/hold_to_speed_off</item>
242+
<item>1.25x</item>
243+
<item>1.5x</item>
244+
<item>1.75x</item>
245+
<item>2x</item>
246+
<item>2.5x</item>
247+
<item>3x</item>
248+
</string-array>
249+
<string-array name="hold_to_speed_values">
250+
<item>@string/hold_to_speed_off_value</item>
251+
<item>1.25</item>
252+
<item>1.5</item>
253+
<item>1.75</item>
254+
<item>2.0</item>
255+
<item>2.5</item>
256+
<item>3.0</item>
257+
</string-array>
258+
235259
<string name="prefer_original_audio_key">prefer_original_audio</string>
236260
<string name="prefer_descriptive_audio_key">prefer_descriptive_audio</string>
237261
<string name="last_resize_mode">last_resize_mode</string>

app/src/main/res/values/strings.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,4 +943,11 @@
943943
<string name="api23_requirement_dialog_title">NewPipe is dropping support for Android 5</string>
944944
<string name="api23_requirement_dialog_message">Unfortunately NewPipe depends on a few libraries that dropped support for Android 5.0 and 5.1. The next NewPipe release will therefore only work on devices with Android 6 or higher, sadly. Read more in the blogpost.</string>
945945
<string name="api23_requirement_dialog_blogpost">Blogpost</string>
946+
<string name="swipe_seek_title">Swipe seek</string>
947+
<string name="swipe_seek_summary">Swipe left or right on the video to seek</string>
948+
<string name="fullscreen_swipe_title">Fullscreen swipe</string>
949+
<string name="fullscreen_swipe_summary">Swipe up or down on the video to enter or exit fullscreen</string>
950+
<string name="hold_to_speed_title">Hold to speed up</string>
951+
<string name="hold_to_speed_summary">Temporarily speed up playback while holding the video</string>
952+
<string name="hold_to_speed_off">Off</string>
946953
</resources>

app/src/main/res/xml/video_audio_settings.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,33 @@
208208
app:singleLineTitle="false"
209209
app:iconSpaceReserved="false" />
210210

211+
<SwitchPreferenceCompat
212+
android:defaultValue="true"
213+
android:key="@string/swipe_seek_gesture_control_key"
214+
android:summary="@string/swipe_seek_summary"
215+
android:title="@string/swipe_seek_title"
216+
app:singleLineTitle="false"
217+
app:iconSpaceReserved="false" />
218+
219+
<SwitchPreferenceCompat
220+
android:defaultValue="true"
221+
android:key="@string/fullscreen_gesture_control_key"
222+
android:summary="@string/fullscreen_swipe_summary"
223+
android:title="@string/fullscreen_swipe_title"
224+
app:singleLineTitle="false"
225+
app:iconSpaceReserved="false" />
226+
227+
<ListPreference
228+
android:defaultValue="@string/hold_to_speed_default_value"
229+
android:entries="@array/hold_to_speed_entries"
230+
android:entryValues="@array/hold_to_speed_values"
231+
android:key="@string/hold_to_speed_key"
232+
android:summary="@string/hold_to_speed_summary"
233+
android:title="@string/hold_to_speed_title"
234+
app:singleLineTitle="false"
235+
app:iconSpaceReserved="false"
236+
app:useSimpleSummaryProvider="true" />
237+
211238
<SwitchPreferenceCompat
212239
android:defaultValue="true"
213240
android:key="@string/popup_remember_size_pos_key"

0 commit comments

Comments
 (0)