mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Some minor optimizations to improve UI speed (#593)
## Description Many tiny changes that improve UI rendering speed and reduces recompositions Still plenty of room for improvement though.
This commit is contained in:
parent
622a99631b
commit
243e08b635
10 changed files with 62 additions and 110 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.github.damontecres.wholphin.ui.detail.CardGridItem
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||
|
|
@ -17,6 +18,7 @@ import org.jellyfin.sdk.model.extensions.ticks
|
|||
import kotlin.time.Duration
|
||||
|
||||
@Serializable
|
||||
@Stable
|
||||
data class BaseItem(
|
||||
val data: BaseItemDto,
|
||||
val useSeriesForPrimary: Boolean,
|
||||
|
|
|
|||
|
|
@ -8,22 +8,15 @@ import androidx.compose.foundation.lazy.LazyRow
|
|||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
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.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.util.FocusPair
|
||||
|
||||
@Composable
|
||||
fun <T> ItemRow(
|
||||
|
|
@ -39,13 +32,10 @@ fun <T> ItemRow(
|
|||
onLongClick: () -> Unit,
|
||||
) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
focusPair: FocusPair? = null,
|
||||
cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null,
|
||||
horizontalPadding: Dp = 16.dp,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
var focusedIndex by remember { mutableIntStateOf(focusPair?.column ?: 0) }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
|
|
@ -62,23 +52,14 @@ fun <T> ItemRow(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRestorer(focusPair?.focusRequester ?: firstFocus),
|
||||
.focusRestorer(firstFocus),
|
||||
) {
|
||||
itemsIndexed(items) { index, item ->
|
||||
val cardModifier =
|
||||
if (index == 0 && focusPair == null) {
|
||||
if (index == 0) {
|
||||
Modifier.focusRequester(firstFocus)
|
||||
} else {
|
||||
if (focusPair != null && focusPair.column == index) {
|
||||
Modifier.focusRequester(focusPair.focusRequester)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
}.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
focusedIndex = index
|
||||
}
|
||||
cardOnFocus?.invoke(it.isFocused, index)
|
||||
Modifier
|
||||
}
|
||||
cardContent.invoke(
|
||||
index,
|
||||
|
|
@ -91,38 +72,3 @@ fun <T> ItemRow(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BannerItemRow(
|
||||
title: String,
|
||||
items: List<BaseItem?>,
|
||||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
focusPair: FocusPair? = null,
|
||||
cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null,
|
||||
aspectRatioOverride: Float? = null,
|
||||
) = ItemRow(
|
||||
title = title,
|
||||
items = items,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
modifier = modifier,
|
||||
cardContent = { index, item, modifier, onClick, onLongClick ->
|
||||
BannerCard(
|
||||
name = title,
|
||||
item = item,
|
||||
aspectRatio =
|
||||
aspectRatioOverride ?: item?.aspectRatio ?: AspectRatios.WIDE,
|
||||
cornerText = item?.data?.indexNumber?.let { "E$it" },
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
playPercent = item?.data?.userData?.playedPercentage ?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier,
|
||||
interactionSource = null,
|
||||
)
|
||||
},
|
||||
focusPair = focusPair,
|
||||
cardOnFocus = cardOnFocus,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.components
|
|||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -21,7 +22,7 @@ fun MovieQuickDetails(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val now = LocalClock.current.now
|
||||
val now by LocalClock.current.now
|
||||
val details =
|
||||
remember(dto, now) {
|
||||
buildList {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -59,7 +60,7 @@ fun EpisodeQuickDetails(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val now = LocalClock.current.now
|
||||
val now by LocalClock.current.now
|
||||
val details =
|
||||
remember(dto, now) {
|
||||
buildList {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.components
|
|||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -13,8 +14,9 @@ import com.github.damontecres.wholphin.ui.util.LocalClock
|
|||
|
||||
@Composable
|
||||
fun BoxScope.TimeDisplay(modifier: Modifier = Modifier) {
|
||||
val timeString by LocalClock.current.timeString
|
||||
Text(
|
||||
text = LocalClock.current.timeString,
|
||||
text = timeString,
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ fun PlaylistItem(
|
|||
},
|
||||
trailingContent = {
|
||||
item?.data?.runTimeTicks?.ticks?.roundMinutes?.let { duration ->
|
||||
val now = LocalClock.current.now
|
||||
val now by LocalClock.current.now
|
||||
val endTimeStr =
|
||||
remember(item, now) {
|
||||
val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
|||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
||||
|
|
@ -226,14 +225,15 @@ fun HomePageContent(
|
|||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
||||
mutableStateOf(RowColumn(firstRow, 0))
|
||||
}
|
||||
var focusedItem =
|
||||
position.let {
|
||||
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
|
||||
val focusedItem =
|
||||
remember(position) {
|
||||
position.let {
|
||||
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val positionFocusRequester = remember { FocusRequester() }
|
||||
val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(homeRows) {
|
||||
if (!focused) {
|
||||
|
|
@ -241,7 +241,7 @@ fun HomePageContent(
|
|||
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
||||
.takeIf { it >= 0 }
|
||||
?.let {
|
||||
positionFocusRequester.tryRequestFocus()
|
||||
rowFocusRequesters[it].tryRequestFocus()
|
||||
delay(50)
|
||||
listState.animateScrollToItem(position.row)
|
||||
focused = true
|
||||
|
|
@ -251,9 +251,6 @@ fun HomePageContent(
|
|||
LaunchedEffect(position) {
|
||||
listState.animateScrollToItem(position.row)
|
||||
}
|
||||
LaunchedEffect(focusedItem) {
|
||||
focusedItem?.let(onUpdateBackdrop)
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
HomePageHeader(
|
||||
|
|
@ -275,16 +272,7 @@ fun HomePageContent(
|
|||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer()
|
||||
.onKeyEvent {
|
||||
val item = focusedItem
|
||||
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
||||
Timber.v("Clicked play on ${item.id}")
|
||||
onClickPlay.invoke(position, item)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
return@onKeyEvent false
|
||||
},
|
||||
.focusRestorer(),
|
||||
) {
|
||||
itemsIndexed(homeRows) { rowIndex, row ->
|
||||
when (val r = row) {
|
||||
|
|
@ -347,18 +335,13 @@ fun HomePageContent(
|
|||
onClickItem = { index, item ->
|
||||
onClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||
},
|
||||
cardOnFocus = { isFocused, index ->
|
||||
if (isFocused) {
|
||||
focusedItem = row.items.getOrNull(index)
|
||||
position = RowColumn(rowIndex, index)
|
||||
}
|
||||
},
|
||||
onLongClickItem = { index, item ->
|
||||
onLongClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(rowFocusRequesters[rowIndex])
|
||||
.animateItem(),
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
val cornerText =
|
||||
|
|
@ -385,29 +368,32 @@ fun HomePageContent(
|
|||
onLongClick = onLongClick,
|
||||
modifier =
|
||||
cardModifier
|
||||
.ifElse(
|
||||
focusedItem == item,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).ifElse(
|
||||
RowColumn(rowIndex, index) == position,
|
||||
Modifier.focusRequester(
|
||||
positionFocusRequester,
|
||||
),
|
||||
).onFocusChanged {
|
||||
.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(
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
|
|
@ -37,11 +36,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
|
|
@ -346,10 +345,8 @@ fun NavDrawer(
|
|||
Modifier
|
||||
.fillMaxHeight()
|
||||
.width(drawerWidth)
|
||||
.background(drawerBackground)
|
||||
.onFocusChanged {
|
||||
if (!it.hasFocus) {
|
||||
}
|
||||
.drawBehind {
|
||||
drawRect(drawerBackground)
|
||||
},
|
||||
) {
|
||||
// Even though some must be clicked, focusing on it should clear other focused items
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ package com.github.damontecres.wholphin.ui.util
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.github.damontecres.wholphin.ui.TimeFormatter
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
|
@ -22,20 +21,28 @@ data class Clock(
|
|||
/**
|
||||
* The current [LocalDateTime]
|
||||
*/
|
||||
val now: LocalDateTime,
|
||||
val now: MutableState<LocalDateTime>,
|
||||
/**
|
||||
* The current time formatted as a string with [TimeFormatter]
|
||||
*/
|
||||
val timeString: String,
|
||||
val timeString: MutableState<String>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ProvideLocalClock(content: @Composable () -> Unit) {
|
||||
var clock by remember { mutableStateOf(LocalDateTime.now().let { Clock(it, TimeFormatter.format(it)) }) }
|
||||
val clock =
|
||||
remember {
|
||||
LocalDateTime
|
||||
.now()
|
||||
.let { Clock(mutableStateOf(it), mutableStateOf(TimeFormatter.format(it))) }
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
while (isActive) {
|
||||
clock = LocalDateTime.now().let { Clock(it, TimeFormatter.format(it)) }
|
||||
delay(1_000)
|
||||
val now = LocalDateTime.now()
|
||||
val time = TimeFormatter.format(now)
|
||||
clock.now.value = now
|
||||
clock.timeString.value = time
|
||||
delay(2_000)
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(LocalClock provides clock, content)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,16 @@ class TestStreamChoiceServiceBasic(
|
|||
),
|
||||
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.UNSPECIFIED),
|
||||
),
|
||||
TestInput(
|
||||
1,
|
||||
SubtitlePlaybackMode.ALWAYS,
|
||||
subtitles =
|
||||
listOf(
|
||||
subtitle(0, "eng", forced = true, default = true),
|
||||
subtitle(1, "eng", false),
|
||||
subtitle(2, "eng", false),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue