Skip to content

Commit b5d3a74

Browse files
committed
Autoscroll when dragging close to border
1 parent 3b49138 commit b5d3a74

File tree

1 file changed

+52
-1
lines changed

1 file changed

+52
-1
lines changed

app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.util.Log
2222
import androidx.annotation.StringRes
2323
import androidx.compose.foundation.BorderStroke
2424
import androidx.compose.foundation.border
25+
import androidx.compose.foundation.gestures.scrollBy
2526
import androidx.compose.foundation.layout.BoxWithConstraints
2627
import androidx.compose.foundation.layout.Column
2728
import androidx.compose.foundation.layout.fillMaxWidth
@@ -33,6 +34,7 @@ import androidx.compose.foundation.layout.width
3334
import androidx.compose.foundation.lazy.grid.GridCells
3435
import androidx.compose.foundation.lazy.grid.GridItemSpan
3536
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
37+
import androidx.compose.foundation.lazy.grid.LazyGridState
3638
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
3739
import androidx.compose.foundation.lazy.grid.itemsIndexed
3840
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
@@ -49,6 +51,7 @@ import androidx.compose.material3.Text
4951
import androidx.compose.runtime.Composable
5052
import androidx.compose.runtime.DisposableEffect
5153
import androidx.compose.runtime.getValue
54+
import androidx.compose.runtime.mutableFloatStateOf
5255
import androidx.compose.runtime.mutableIntStateOf
5356
import androidx.compose.runtime.mutableStateOf
5457
import androidx.compose.runtime.remember
@@ -77,6 +80,9 @@ import androidx.compose.ui.unit.IntOffset
7780
import androidx.compose.ui.unit.IntSize
7881
import androidx.compose.ui.unit.dp
7982
import androidx.compose.ui.unit.toSize
83+
import kotlinx.coroutines.Job
84+
import kotlinx.coroutines.delay
85+
import kotlinx.coroutines.isActive
8086
import kotlinx.coroutines.launch
8187
import org.schabi.newpipe.R
8288
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.Companion.DefaultEnabledActions
@@ -85,6 +91,7 @@ import org.schabi.newpipe.ui.theme.AppTheme
8591
import org.schabi.newpipe.util.text.FixedHeightCenteredText
8692
import kotlin.math.abs
8793
import kotlin.math.floor
94+
import kotlin.math.max
8895
import kotlin.math.min
8996

9097
const val TAG = "LongPressMenuEditor"
@@ -120,12 +127,15 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
120127
}.toList().toMutableStateList()
121128
}
122129

123-
val coroutineScope = rememberCoroutineScope()
130+
// variables for handling drag, focus, and autoscrolling when finger is at top/bottom
124131
val gridState = rememberLazyGridState()
125132
var activeDragItem by remember { mutableStateOf<ItemInList?>(null) }
126133
var activeDragPosition by remember { mutableStateOf(IntOffset.Zero) }
127134
var activeDragSize by remember { mutableStateOf(IntSize.Zero) }
128135
var currentlyFocusedItem by remember { mutableIntStateOf(-1) }
136+
val coroutineScope = rememberCoroutineScope()
137+
var autoScrollJob by remember { mutableStateOf<Job?>(null) }
138+
var autoScrollSpeed by remember { mutableFloatStateOf(0f) } // -1, 0 or 1
129139

130140
fun findItemForOffsetOrClosestInRow(offset: IntOffset): LazyGridItemInfo? {
131141
var closestItemInRow: LazyGridItemInfo? = null
@@ -143,6 +153,7 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
143153
return closestItemInRow
144154
}
145155

156+
// called not just for drag gestures initiated by moving the finger, but also with DPAD's Enter
146157
fun beginDragGesture(pos: IntOffset, rawItem: LazyGridItemInfo) {
147158
if (activeDragItem != null) return
148159
val item = items.getOrNull(rawItem.index) ?: return
@@ -154,11 +165,22 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
154165
}
155166
}
156167

168+
// this beginDragGesture() overload is only called when moving the finger (not on DPAD's Enter)
157169
fun beginDragGesture(pos: IntOffset) {
158170
val rawItem = findItemForOffsetOrClosestInRow(pos) ?: return
159171
beginDragGesture(pos, rawItem)
172+
autoScrollSpeed = 0f
173+
autoScrollJob = coroutineScope.launch {
174+
while (isActive) {
175+
if (autoScrollSpeed != 0f) {
176+
gridState.scrollBy(autoScrollSpeed)
177+
}
178+
delay(16L) // roughly 60 FPS
179+
}
180+
}
160181
}
161182

183+
// called not just for drag gestures by moving the finger, but also with DPAD's events
162184
fun handleDragGestureChange(dragItem: ItemInList, rawItem: LazyGridItemInfo) {
163185
val prevDragMarkerIndex = items.indexOfFirst { it is ItemInList.DragMarker }
164186
.takeIf { it >= 0 } ?: return // impossible situation, DragMarker is always in the list
@@ -202,19 +224,27 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
202224
}
203225
}
204226

227+
// this handleDragGestureChange() overload is only called when moving the finger
228+
// (not on DPAD's events)
205229
fun handleDragGestureChange(pos: IntOffset, posChangeForScrolling: Offset) {
206230
val dragItem = activeDragItem
207231
if (dragItem == null) {
208232
// when the user clicks outside of any draggable item, let the list be scrolled
209233
gridState.dispatchRawDelta(-posChangeForScrolling.y)
210234
return
211235
}
236+
autoScrollSpeed = autoScrollSpeedFromTouchPos(pos, gridState)
212237
activeDragPosition = pos
213238
val rawItem = findItemForOffsetOrClosestInRow(pos) ?: return
214239
handleDragGestureChange(dragItem, rawItem)
215240
}
216241

242+
// called in multiple places both, e.g. when the finger stops touching, or with DPAD events
217243
fun completeDragGestureAndCleanUp() {
244+
autoScrollJob?.cancel()
245+
autoScrollJob = null
246+
autoScrollSpeed = 0f
247+
218248
val dragItem = activeDragItem
219249
if (dragItem != null) {
220250
val dragMarkerIndex = items.indexOfFirst { it is ItemInList.DragMarker }
@@ -410,6 +440,27 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
410440
}
411441
}
412442

443+
fun autoScrollSpeedFromTouchPos(
444+
touchPos: IntOffset,
445+
gridState: LazyGridState,
446+
maxSpeed: Float = 20f,
447+
scrollIfCloseToBorderPercent: Float = 0.1f,
448+
): Float {
449+
val heightPosRatio = touchPos.y.toFloat() /
450+
(gridState.layoutInfo.viewportEndOffset - gridState.layoutInfo.viewportStartOffset)
451+
// just a linear piecewise function, sets higher speeds the closer the finger is to the border
452+
return maxSpeed * max(
453+
// proportionally positive speed when close to the bottom border
454+
(heightPosRatio - 1) / scrollIfCloseToBorderPercent + 1,
455+
min(
456+
// proportionally negative speed when close to the top border
457+
heightPosRatio / scrollIfCloseToBorderPercent - 1,
458+
// don't scroll at all if not close to any border
459+
0f
460+
)
461+
)
462+
}
463+
413464
sealed class ItemInList(val isDraggable: Boolean, open val columnSpan: Int? = 1) {
414465
// decoration items (i.e. text subheaders)
415466
object EnabledCaption : ItemInList(isDraggable = false, columnSpan = null /* i.e. all line */)

0 commit comments

Comments
 (0)