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