Skip to content

Commit 738af9a

Browse files
authored
Merge pull request #7757 from osamasayed585/feat/drag-to-seekbar
feat: allow drag-to-seek like YouTube
2 parents 16f7c95 + 5d6fa02 commit 738af9a

1 file changed

Lines changed: 84 additions & 10 deletions

File tree

app/src/main/java/com/github/libretube/ui/views/DismissableTimeBar.kt

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import android.annotation.SuppressLint
44
import android.content.Context
55
import android.util.AttributeSet
66
import android.view.MotionEvent
7+
import android.view.ViewConfiguration
78
import androidx.media3.common.Player
89
import androidx.media3.common.util.UnstableApi
910
import androidx.media3.ui.DefaultTimeBar
1011
import androidx.media3.ui.PlayerControlView
1112
import androidx.media3.ui.TimeBar
1213
import androidx.media3.ui.TimeBar.OnScrubListener
1314
import com.github.libretube.extensions.dpToPx
15+
import kotlin.math.abs
1416

1517
@UnstableApi
1618
open class DismissableTimeBar(
@@ -19,41 +21,113 @@ open class DismissableTimeBar(
1921
): DefaultTimeBar(context, attributeSet) {
2022
private var shouldAddListener = false
2123
var exoPlayer: Player? = null
22-
private var lastYPosition = 0f
24+
25+
private val listeners = mutableListOf<OnScrubListener>()
26+
27+
// Drag-only seeking state
28+
private var initialX: Float = 0f
29+
private var initialY: Float = 0f
30+
private var waitingForDrag: Boolean = false
31+
private var dragStarted: Boolean = false
32+
private val touchSlopPx: Int = ViewConfiguration.get(context).scaledTouchSlop
2333

2434
init {
25-
addSeekBarListener(object : OnScrubListener {
26-
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
35+
super.addListener(object : OnScrubListener {
36+
override fun onScrubStart(timeBar: TimeBar, position: Long) {
37+
listeners.forEach { it.onScrubStart(timeBar, position) }
38+
}
2739

28-
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
40+
override fun onScrubMove(timeBar: TimeBar, position: Long) {
41+
listeners.forEach { it.onScrubMove(timeBar, position) }
42+
}
2943

3044
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
31-
if (lastYPosition > MINIMUM_ACCEPTED_HEIGHT.dpToPx()) exoPlayer?.seekTo(position)
45+
listeners.forEach { it.onScrubStop(timeBar, position, canceled) }
46+
47+
if (canceled) return
48+
// Ignore if gesture started too far above the bar (keep original behavior)
49+
if (initialY <= MINIMUM_ACCEPTED_HEIGHT.dpToPx()) return
50+
51+
exoPlayer?.seekTo(position)
3252
}
3353
})
3454
}
3555

3656
@SuppressLint("ClickableViewAccessibility")
3757
override fun onTouchEvent(event: MotionEvent): Boolean {
38-
lastYPosition = event.y
58+
when (event.actionMasked) {
59+
MotionEvent.ACTION_DOWN -> {
60+
initialX = event.x
61+
initialY = event.y
62+
waitingForDrag = true
63+
dragStarted = false
64+
// Consume without forwarding to prevent tap-to-seek or thumb jump
65+
return true
66+
}
67+
68+
MotionEvent.ACTION_MOVE -> {
69+
if (waitingForDrag) {
70+
// make sure that the user dragged at least touchSlopPx pixels
71+
// which is the minimum amount to trigger a drag event
72+
val dx = abs(event.x - initialX)
73+
val dy = abs(event.y - initialY)
74+
if (dx > touchSlopPx || dy > touchSlopPx) {
75+
// Begin scrubbing now by synthesizing a DOWN at the current progress X
76+
val fakeDown = MotionEvent.obtain(event).apply {
77+
action = MotionEvent.ACTION_DOWN
78+
setLocation(event.x, event.y)
79+
}
80+
super.onTouchEvent(fakeDown)
81+
fakeDown.recycle()
82+
83+
waitingForDrag = false
84+
dragStarted = true
85+
} else {
86+
// Still not considered a drag; consume
87+
return true
88+
}
89+
}
90+
91+
// Forward MOVE only after we've started dragging, with relative X
92+
if (dragStarted) {
93+
return super.onTouchEvent(event)
94+
}
95+
return true
96+
}
97+
98+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
99+
// If we started a drag, forward the terminal event to finish scrubbing
100+
if (dragStarted) {
101+
waitingForDrag = false
102+
dragStarted = false
103+
return super.onTouchEvent(event)
104+
}
105+
106+
// It's a tap without drag: do nothing (prevent seek and thumb jump)
107+
waitingForDrag = false
108+
dragStarted = false
109+
// Optionally report click for accessibility without side effects
110+
performClick()
111+
return true
112+
}
113+
}
39114

40115
return super.onTouchEvent(event)
41116
}
42117

43118
/**
44119
* DO NOT CALL THIS METHOD DIRECTLY. Use [addSeekBarListener] instead!
45120
*/
121+
@Deprecated("Use addSeekBarListener instead")
46122
override fun addListener(listener: OnScrubListener) {
47-
if (shouldAddListener) super.addListener(listener)
123+
// do nothing, see below on how listeners should be set
48124
}
49125

50126
/**
51127
* Wrapper to circumvent adding the listener created by [PlayerControlView]
52128
*/
53129
fun addSeekBarListener(listener: OnScrubListener) {
54-
shouldAddListener = true
55-
addListener(listener)
56-
shouldAddListener = false
130+
listeners.add(listener)
57131
}
58132

59133
fun setPlayer(player: Player) {

0 commit comments

Comments
 (0)