@@ -4,13 +4,15 @@ import android.annotation.SuppressLint
44import android.content.Context
55import android.util.AttributeSet
66import android.view.MotionEvent
7+ import android.view.ViewConfiguration
78import androidx.media3.common.Player
89import androidx.media3.common.util.UnstableApi
910import androidx.media3.ui.DefaultTimeBar
1011import androidx.media3.ui.PlayerControlView
1112import androidx.media3.ui.TimeBar
1213import androidx.media3.ui.TimeBar.OnScrubListener
1314import com.github.libretube.extensions.dpToPx
15+ import kotlin.math.abs
1416
1517@UnstableApi
1618open 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