mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Fixes glitches when scrolling on the home page (#882)
## Description Fixes the possible glitch on some devices when scrolling left-right on rows on the home page ### Related issues Fixes #367 Fixes #872 ### Testing Emulator & shield 2019 ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
6e676b9474
commit
83e5a44f37
3 changed files with 236 additions and 152 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package com.github.damontecres.wholphin.ui.discover
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
|
@ -11,6 +13,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -23,6 +26,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
|
|
@ -47,6 +51,7 @@ import com.github.damontecres.wholphin.ui.listToDotString
|
|||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -186,6 +191,7 @@ data class DiscoverState(
|
|||
val upcomingTv: DiscoverRowData = DiscoverRowData.EMPTY,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SeerrDiscoverPage(
|
||||
preferences: UserPreferences,
|
||||
|
|
@ -249,28 +255,41 @@ fun SeerrDiscoverPage(
|
|||
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
||||
.fillMaxHeight(.25f),
|
||||
)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 40.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer()
|
||||
.fillMaxSize(),
|
||||
val density = LocalDensity.current
|
||||
val spaceAbovePx =
|
||||
with(density) {
|
||||
// The size of the row titles & spacing
|
||||
50.dp.toPx()
|
||||
}
|
||||
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||
CompositionLocalProvider(
|
||||
LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx),
|
||||
) {
|
||||
itemsIndexed(rows) { rowIndex, row ->
|
||||
DiscoverRow(
|
||||
row = row,
|
||||
onClickItem = { index, item ->
|
||||
position = RowColumn(rowIndex, index)
|
||||
viewModel.navigationManager.navigateTo(item.destination)
|
||||
},
|
||||
onLongClickItem = { index, item -> },
|
||||
onCardFocus = { index -> position = RowColumn(rowIndex, index) },
|
||||
focusRequester = focusRequesters[rowIndex],
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 40.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer()
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
itemsIndexed(rows) { rowIndex, row ->
|
||||
CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||
DiscoverRow(
|
||||
row = row,
|
||||
onClickItem = { index, item ->
|
||||
position = RowColumn(rowIndex, index)
|
||||
viewModel.navigationManager.navigateTo(item.destination)
|
||||
},
|
||||
onLongClickItem = { index, item -> },
|
||||
onCardFocus = { index -> position = RowColumn(rowIndex, index) },
|
||||
focusRequester = focusRequesters[rowIndex],
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.github.damontecres.wholphin.ui.main
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -19,6 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
|
|
@ -34,6 +37,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -69,6 +73,7 @@ import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
|||
import com.github.damontecres.wholphin.ui.playback.playable
|
||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -183,6 +188,7 @@ fun HomePage(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HomePageContent(
|
||||
homeRows: List<HomeRowLoadingState>,
|
||||
|
|
@ -223,14 +229,15 @@ fun HomePageContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(position) {
|
||||
if (position.row >= 0) {
|
||||
listState.animateScrollToItem(position.row)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(onUpdateBackdrop, focusedItem) {
|
||||
focusedItem?.let { onUpdateBackdrop.invoke(it) }
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val spaceAbovePx =
|
||||
with(density) {
|
||||
// The size of the row titles & spacing
|
||||
50.dp.toPx()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
HomePageHeader(
|
||||
|
|
@ -240,134 +247,148 @@ fun HomePageContent(
|
|||
.padding(top = 48.dp, bottom = 32.dp, start = 8.dp)
|
||||
.fillMaxHeight(.33f),
|
||||
)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
bottom = Cards.height2x3,
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer(),
|
||||
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||
CompositionLocalProvider(
|
||||
LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx),
|
||||
) {
|
||||
itemsIndexed(homeRows) { rowIndex, row ->
|
||||
when (val r = row) {
|
||||
is HomeRowLoadingState.Loading,
|
||||
is HomeRowLoadingState.Pending,
|
||||
-> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.loading),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowLoadingState.Error -> {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
focused = it.isFocused
|
||||
}.focusable()
|
||||
.background(
|
||||
if (focused) {
|
||||
// Just so the user can tell it has focus
|
||||
MaterialTheme.colorScheme.border.copy(alpha = .25f)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
},
|
||||
).animateItem(),
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = r.localizedMessage,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowLoadingState.Success -> {
|
||||
if (row.items.isNotEmpty()) {
|
||||
ItemRow(
|
||||
title = row.title,
|
||||
items = row.items,
|
||||
onClickItem = { index, item ->
|
||||
onClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||
},
|
||||
onLongClickItem = { index, item ->
|
||||
onLongClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusGroup()
|
||||
.focusRequester(rowFocusRequesters[rowIndex])
|
||||
.animateItem(),
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
item = item,
|
||||
aspectRatio = AspectRatios.TALL,
|
||||
cornerText = item?.ui?.episodeUnplayedCornerText,
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
favorite = item?.favorite ?: false,
|
||||
playPercent =
|
||||
item?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier =
|
||||
cardModifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
position = RowColumn(rowIndex, index)
|
||||
// item?.let(onUpdateBackdrop)
|
||||
}
|
||||
if (it.isFocused && onFocusPosition != null) {
|
||||
val nonEmptyRowBefore =
|
||||
homeRows
|
||||
.subList(0, rowIndex)
|
||||
.count {
|
||||
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
||||
}
|
||||
onFocusPosition.invoke(
|
||||
RowColumn(
|
||||
rowIndex - nonEmptyRowBefore,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.onKeyEvent {
|
||||
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
||||
Timber.v("Clicked play on ${item.id}")
|
||||
onClickPlay.invoke(position, item)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
return@onKeyEvent false
|
||||
},
|
||||
interactionSource = null,
|
||||
cardHeight = Cards.height2x3,
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
bottom = Cards.height2x3,
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer(),
|
||||
) {
|
||||
itemsIndexed(homeRows) { rowIndex, row ->
|
||||
CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||
when (val r = row) {
|
||||
is HomeRowLoadingState.Loading,
|
||||
is HomeRowLoadingState.Pending,
|
||||
-> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.loading),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowLoadingState.Error -> {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
focused = it.isFocused
|
||||
}.focusable()
|
||||
.background(
|
||||
if (focused) {
|
||||
// Just so the user can tell it has focus
|
||||
MaterialTheme.colorScheme.border.copy(alpha = .25f)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
},
|
||||
).animateItem(),
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = r.localizedMessage,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowLoadingState.Success -> {
|
||||
if (row.items.isNotEmpty()) {
|
||||
ItemRow(
|
||||
title = row.title,
|
||||
items = row.items,
|
||||
onClickItem = { index, item ->
|
||||
onClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||
},
|
||||
onLongClickItem = { index, item ->
|
||||
onLongClickItem.invoke(
|
||||
RowColumn(rowIndex, index),
|
||||
item,
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusGroup()
|
||||
.focusRequester(rowFocusRequesters[rowIndex])
|
||||
.animateItem(),
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
item = item,
|
||||
aspectRatio = AspectRatios.TALL,
|
||||
cornerText = item?.ui?.episodeUnplayedCornerText,
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
favorite = item?.favorite ?: false,
|
||||
playPercent =
|
||||
item?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier =
|
||||
cardModifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
position =
|
||||
RowColumn(rowIndex, index)
|
||||
// item?.let(onUpdateBackdrop)
|
||||
}
|
||||
if (it.isFocused && onFocusPosition != null) {
|
||||
val nonEmptyRowBefore =
|
||||
homeRows
|
||||
.subList(0, rowIndex)
|
||||
.count {
|
||||
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
||||
}
|
||||
onFocusPosition.invoke(
|
||||
RowColumn(
|
||||
rowIndex - nonEmptyRowBefore,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.onKeyEvent {
|
||||
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
||||
Timber.v("Clicked play on ${item.id}")
|
||||
onClickPlay.invoke(
|
||||
position,
|
||||
item,
|
||||
)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
return@onKeyEvent false
|
||||
},
|
||||
interactionSource = null,
|
||||
cardHeight = Cards.height2x3,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.github.damontecres.wholphin.ui.util
|
||||
|
||||
import androidx.compose.foundation.gestures.BringIntoViewSpec
|
||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
||||
/**
|
||||
* Overrides scrolling so that the item being scrolled to is at the top of the view offset by the provided pixels
|
||||
*
|
||||
* Note: the offset is necessary for anything that is focuseable, but has content before (eg a title) that needs to be displayed too
|
||||
*
|
||||
* Note: this applies to ALL scrollable composables within its scope, so a [LazyColumn] of [androidx.compose.foundation.lazy.LazyRow]s likely needs nested [LocalBringIntoViewSpec] overrides
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||
* CompositionLocalProvider(LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx)) {
|
||||
* LazyColumn{
|
||||
* items(list){
|
||||
* CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||
* // Content
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class ScrollToTopBringIntoViewSpec(
|
||||
val spaceAbovePx: Float = 100f,
|
||||
) : BringIntoViewSpec {
|
||||
override fun calculateScrollDistance(
|
||||
offset: Float,
|
||||
size: Float,
|
||||
containerSize: Float,
|
||||
): Float {
|
||||
// Timber.v(
|
||||
// "calculateScrollDistance: offset=%s, size=%s, containerSize=%s",
|
||||
// offset,
|
||||
// size,
|
||||
// containerSize,
|
||||
// )
|
||||
return offset - spaceAbovePx
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue