@@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
99import androidx.appcompat.content.res.AppCompatResources
1010import androidx.core.view.isVisible
1111import kotlin.math.abs
12+ import kotlin.math.sign
1213import org.schabi.newpipe.MainActivity
1314import org.schabi.newpipe.R
1415import org.schabi.newpipe.ktx.AnimationType
@@ -17,6 +18,7 @@ import org.schabi.newpipe.player.Player
1718import org.schabi.newpipe.player.helper.AudioReactor
1819import org.schabi.newpipe.player.helper.PlayerHelper
1920import org.schabi.newpipe.player.ui.MainPlayerUi
21+ import org.schabi.newpipe.util.Localization
2022import 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}
0 commit comments