Skip to content

Commit 3b49138

Browse files
committed
Handle scrolling on Android TV
1 parent d2b5f9f commit 3b49138

1 file changed

Lines changed: 40 additions & 7 deletions

File tree

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

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package org.schabi.newpipe.ui.components.menu
2020

21+
import android.util.Log
2122
import androidx.annotation.StringRes
2223
import androidx.compose.foundation.BorderStroke
2324
import androidx.compose.foundation.border
@@ -51,6 +52,7 @@ import androidx.compose.runtime.getValue
5152
import androidx.compose.runtime.mutableIntStateOf
5253
import androidx.compose.runtime.mutableStateOf
5354
import androidx.compose.runtime.remember
55+
import androidx.compose.runtime.rememberCoroutineScope
5456
import androidx.compose.runtime.setValue
5557
import androidx.compose.runtime.toMutableStateList
5658
import androidx.compose.ui.Alignment
@@ -75,16 +77,19 @@ import androidx.compose.ui.unit.IntOffset
7577
import androidx.compose.ui.unit.IntSize
7678
import androidx.compose.ui.unit.dp
7779
import androidx.compose.ui.unit.toSize
80+
import kotlinx.coroutines.launch
7881
import org.schabi.newpipe.R
7982
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.Companion.DefaultEnabledActions
8083
import org.schabi.newpipe.ui.detectDragGestures
8184
import org.schabi.newpipe.ui.theme.AppTheme
8285
import org.schabi.newpipe.util.text.FixedHeightCenteredText
86+
import kotlin.math.abs
8387
import kotlin.math.floor
8488
import kotlin.math.min
8589

90+
const val TAG = "LongPressMenuEditor"
91+
8692
// TODO padding doesn't seem to work as expected when the list becomes scrollable?
87-
// TODO does Android TV auto-scroll to the selected item when the list becomes scrollable?
8893
@Composable
8994
fun LongPressMenuEditor(modifier: Modifier = Modifier) {
9095
// We get the current arrangement once and do not observe on purpose
@@ -115,6 +120,7 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
115120
}.toList().toMutableStateList()
116121
}
117122

123+
val coroutineScope = rememberCoroutineScope()
118124
val gridState = rememberLazyGridState()
119125
var activeDragItem by remember { mutableStateOf<ItemInList?>(null) }
120126
var activeDragPosition by remember { mutableStateOf(IntOffset.Zero) }
@@ -173,7 +179,7 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
173179
// make sure it is not possible to move items in between a *Caption and a HeaderBox
174180
if (!items[i].isDraggable) i += 1
175181
if (i < items.size && items[i] == ItemInList.HeaderBox) i += 1
176-
if (i > rawItem.index && prevDragMarkerIndex < rawItem.index) i -= 1
182+
if (rawItem.index in (prevDragMarkerIndex + 1)..<i) i -= 1
177183
i
178184
}
179185

@@ -228,9 +234,11 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
228234
}
229235
}
230236

237+
// test scrolling on Android TV by adding `.padding(horizontal = 350.dp)` here
231238
BoxWithConstraints(modifier) {
232239
// otherwise we wouldn't know the amount of columns to handle the Up/Down key events
233240
val columns = maxOf(1, floor(this.maxWidth / MinButtonWidth).toInt())
241+
234242
LazyVerticalGrid(
235243
modifier = Modifier
236244
.safeDrawingPadding()
@@ -239,14 +247,14 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
239247
handleDragGestureChange = ::handleDragGestureChange,
240248
endDragGesture = ::completeDragGestureAndCleanUp,
241249
)
250+
// this huge .focusTarget().onKeyEvent() block just handles DPAD on Android TVs
242251
.focusTarget()
243252
.onKeyEvent { event ->
244253
if (event.type != KeyEventType.KeyDown) {
245254
if (event.type == KeyEventType.KeyUp &&
246255
event.key == Key.DirectionDown &&
247256
currentlyFocusedItem < 0
248257
) {
249-
//
250258
currentlyFocusedItem = 0
251259
}
252260
return@onKeyEvent false
@@ -259,6 +267,7 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
259267
} else if (items[focusedItem].columnSpan == null) {
260268
focusedItem -= 1
261269
} else {
270+
// go to the previous line
262271
var remaining = columns
263272
while (true) {
264273
focusedItem -= 1
@@ -276,9 +285,10 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
276285
Key.DirectionDown -> {
277286
if (focusedItem >= items.size - 1) {
278287
return@onKeyEvent false
279-
} else if (items[focusedItem].columnSpan == null) {
288+
} else if (focusedItem < 0 || items[focusedItem].columnSpan == null) {
280289
focusedItem += 1
281290
} else {
291+
// go to the next line
282292
var remaining = columns
283293
while (true) {
284294
focusedItem += 1
@@ -332,13 +342,36 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
332342
// then there would be no indication of the current cursor position at all.
333343
completeDragGestureAndCleanUp()
334344
return@onKeyEvent false
345+
} else if (focusedItem >= items.size) {
346+
Log.w(TAG, "Invalid focusedItem $focusedItem: >= items size ${items.size}")
347+
}
348+
349+
val rawItem = gridState.layoutInfo.visibleItemsInfo
350+
.minByOrNull { abs(it.index - focusedItem) }
351+
?: return@onKeyEvent false // no item is visible at all, impossible case
352+
353+
// If the item we are going to focus is not visible or is close to the boundary,
354+
// scroll to it. Note that this will cause the "drag item" to appear misplaced,
355+
// since the drag item's position is set to the position of the focused item
356+
// before scrolling. However, it's not worth overcomplicating the logic just for
357+
// correcting the position of a drag hint on Android TVs.
358+
val h = rawItem.size.height
359+
if (rawItem.index != focusedItem ||
360+
rawItem.offset.y <= gridState.layoutInfo.viewportStartOffset + 0.8 * h ||
361+
rawItem.offset.y + 1.8 * h >= gridState.layoutInfo.viewportEndOffset
362+
) {
363+
coroutineScope.launch {
364+
gridState.scrollToItem(focusedItem, -(0.8 * h).toInt())
365+
}
335366
}
336367

337368
val dragItem = activeDragItem
338369
if (dragItem != null) {
339-
val rawItem = gridState.layoutInfo.visibleItemsInfo
340-
.firstOrNull { it.index == focusedItem }
341-
?: return@onKeyEvent false
370+
// This will mostly bring the drag item to the right position, but will
371+
// misplace it if the view just scrolled (see above), or if the DragMarker's
372+
// position is moved past HiddenCaption by handleDragGestureChange() below.
373+
// However, it's not worth overcomplicating the logic just for correcting
374+
// the position of a drag hint on Android TVs.
342375
activeDragPosition = rawItem.offset
343376
handleDragGestureChange(dragItem, rawItem)
344377
}

0 commit comments

Comments
 (0)