fix: correct re-centering animation making flicker

This commit is contained in:
2026-07-02 16:54:56 +02:00
parent 24d39d2a6f
commit c307111ec1
@@ -18,8 +18,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -27,6 +28,7 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.ajaury.gwenedeg.core.model.Phrase import fr.ajaury.gwenedeg.core.model.Phrase
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
@@ -44,19 +46,24 @@ fun SubtitleList(
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val isDragged by listState.interactionSource.collectIsDraggedAsState() val isDragged by listState.interactionSource.collectIsDraggedAsState()
var targetIndex by remember { mutableStateOf(0) }
var isDragging by remember { mutableStateOf(false) }
val centeredIndex by remember { // Detect beginning of dragging.
LaunchedEffect(isDragged) {
if (isDragged) {
isDragging = true
}
}
val centeredIndex by remember(currentIndex, isDragged) {
derivedStateOf { derivedStateOf {
val info = listState.layoutInfo if (isDragging) {
if (info.visibleItemsInfo.isEmpty()) { listState.getCenteredIndex().also {
-1 targetIndex = it
}
} else { } else {
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2f currentIndex ?: -1
info.visibleItemsInfo
.minByOrNull {
abs((it.offset + it.size / 2f) - viewportCenter)
}?.index
?: -1
} }
} }
} }
@@ -69,20 +76,14 @@ fun SubtitleList(
} }
// Snap to the centered line and seek to it once the user finishes scrolling. // Snap to the centered line and seek to it once the user finishes scrolling.
LaunchedEffect(listState, lines) { LaunchedEffect(isDragged) {
var wasDragged = false if (isDragging && !isDragged) {
snapshotFlow { isDragged to listState.isScrollInProgress }
.collect { (dragged, scrolling) ->
if (dragged) {
wasDragged = true
} else if (wasDragged && !scrolling) {
wasDragged = false
val targetIndex = centeredIndex
if (targetIndex in lines.indices) { if (targetIndex in lines.indices) {
listState.centerItem(targetIndex) listState.centerItem(targetIndex)
onSeek(targetIndex) onSeek(targetIndex)
} }
}
isDragging = false
} }
} }
@@ -107,6 +108,20 @@ fun SubtitleList(
} }
} }
private fun LazyListState.getCenteredIndex(): Int {
val info = this.layoutInfo
return if (info.visibleItemsInfo.isEmpty()) {
-1
} else {
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2f
info.visibleItemsInfo
.minByOrNull {
abs((it.offset + it.size / 2f) - viewportCenter)
}?.index
?: -1
}
}
private suspend fun LazyListState.centerItem(index: Int) { private suspend fun LazyListState.centerItem(index: Int) {
if (index < 0 || index >= this.layoutInfo.totalItemsCount) return if (index < 0 || index >= this.layoutInfo.totalItemsCount) return
@@ -126,11 +141,8 @@ private fun Subtitle(
isCurrent: Boolean, isCurrent: Boolean,
subtitlePreferences: SubtitlePreferences = SubtitlePreferences(), subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
) { ) {
val contentColor = if (isCurrent) { val textAlpha = if (isCurrent) 1f else 0.35f
Color.Unspecified val contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = textAlpha)
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
}
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -140,7 +152,6 @@ private fun Subtitle(
if (subtitlePreferences.showTranscription) { if (subtitlePreferences.showTranscription) {
TranscriptionLine( TranscriptionLine(
text = line.phrase.transcription, text = line.phrase.transcription,
isCurrent = isCurrent,
contentColor = contentColor, contentColor = contentColor,
) )
} }
@@ -149,7 +160,6 @@ private fun Subtitle(
?.let { translation -> ?.let { translation ->
TranslationLine( TranslationLine(
text = translation, text = translation,
isCurrent = isCurrent,
contentColor = contentColor, contentColor = contentColor,
) )
} }
@@ -159,18 +169,11 @@ private fun Subtitle(
@Composable @Composable
private fun TranscriptionLine( private fun TranscriptionLine(
text: String, text: String,
isCurrent: Boolean,
contentColor: Color, contentColor: Color,
) { ) {
val mainTextStyle = if (isCurrent) {
MaterialTheme.typography.headlineSmall
} else {
MaterialTheme.typography.bodyLarge
}
Text( Text(
text = text, text = text,
style = mainTextStyle, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp),
color = contentColor, color = contentColor,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -180,18 +183,11 @@ private fun TranscriptionLine(
@Composable @Composable
private fun TranslationLine( private fun TranslationLine(
text: String, text: String,
isCurrent: Boolean,
contentColor: Color, contentColor: Color,
) { ) {
val translationStyle = if (isCurrent) {
MaterialTheme.typography.bodyLarge
} else {
MaterialTheme.typography.bodySmall
}
Text( Text(
text = text, text = text,
style = translationStyle.copy(fontStyle = FontStyle.Italic), style = MaterialTheme.typography.bodySmall.copy(fontStyle = FontStyle.Italic),
color = contentColor, color = contentColor,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),