better pagination
This commit is contained in:
parent
44cc0bbaf3
commit
df90b4c36f
@ -31,6 +31,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@ -77,6 +78,7 @@ import net.sergeych.toread.text.SoftHyphen
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
internal fun ContinuousBookReader(
|
||||
@ -90,6 +92,7 @@ internal fun ContinuousBookReader(
|
||||
) {
|
||||
val hyphenation = remember { HyphenationRegistry() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val textLineMetricsByItem = remember(contentPlan) { mutableStateMapOf<Int, TextLineMetrics>() }
|
||||
val contentPadding = if (isAndroidPlatform()) {
|
||||
PaddingValues(start = 0.dp, top = 6.dp, end = 0.dp, bottom = 6.dp)
|
||||
} else {
|
||||
@ -103,12 +106,12 @@ internal fun ContinuousBookReader(
|
||||
.pageTurnOnTouchTap(
|
||||
onPageDown = {
|
||||
scope.launch {
|
||||
listState.animateScrollBy(listState.pageScrollDistance())
|
||||
listState.pageScrollByPage(1, textLineMetricsByItem)
|
||||
}
|
||||
},
|
||||
onPageUp = {
|
||||
scope.launch {
|
||||
listState.animateScrollBy(-listState.pageScrollDistance())
|
||||
listState.pageScrollByPage(-1, textLineMetricsByItem)
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -175,6 +178,7 @@ internal fun ContinuousBookReader(
|
||||
textAlign = TextAlign.Justify,
|
||||
// so we add 6.dp to make it look symmetric
|
||||
modifier = Modifier.padding(start = (element.depth * 8).dp + 6.dp, end = 0.dp),
|
||||
onTextLayout = { textLineMetricsByItem[itemIndex] = it.toTextLineMetrics() },
|
||||
)
|
||||
is ReaderElement.Subtitle -> ReaderText(
|
||||
text = element.text,
|
||||
@ -184,6 +188,7 @@ internal fun ContinuousBookReader(
|
||||
highlightedRange = highlightedRange,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
|
||||
onTextLayout = { textLineMetricsByItem[itemIndex] = it.toTextLineMetrics() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -195,14 +200,34 @@ private fun Modifier.pageTurnOnTouchTap(
|
||||
onPageUp: () -> Unit,
|
||||
): Modifier = pointerInput(onPageDown, onPageUp) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Final)
|
||||
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||
if (down.type != PointerType.Touch) {
|
||||
waitForUpOrCancellation(pass = PointerEventPass.Final)
|
||||
return@awaitEachGesture
|
||||
}
|
||||
|
||||
val up = waitForUpOrCancellation(pass = PointerEventPass.Final) ?: return@awaitEachGesture
|
||||
if (up.isConsumed) return@awaitEachGesture
|
||||
val touchSlop = viewConfiguration.touchSlop
|
||||
val touchSlopSquared = touchSlop * touchSlop
|
||||
var upPosition: Offset? = null
|
||||
var cancelled = false
|
||||
|
||||
while (upPosition == null && !cancelled) {
|
||||
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||
val change = event.changes.firstOrNull { it.id == down.id }
|
||||
if (change == null) {
|
||||
cancelled = true
|
||||
} else if (change.pressed) {
|
||||
val dx = change.position.x - down.position.x
|
||||
val dy = change.position.y - down.position.y
|
||||
if (dx * dx + dy * dy > touchSlopSquared) {
|
||||
cancelled = true
|
||||
}
|
||||
} else {
|
||||
upPosition = change.position
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled) return@awaitEachGesture
|
||||
|
||||
if (down.position.x < size.width / 2f) {
|
||||
onPageDown()
|
||||
@ -212,12 +237,68 @@ private fun Modifier.pageTurnOnTouchTap(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun LazyListState.pageScrollByPage(
|
||||
direction: Int,
|
||||
textLineMetricsByItem: Map<Int, TextLineMetrics>,
|
||||
) {
|
||||
val target = pageScrollTarget(direction, textLineMetricsByItem)
|
||||
if (target != null) {
|
||||
animateScrollToItem(target.itemIndex, target.scrollOffset)
|
||||
} else {
|
||||
animateScrollBy(pageScrollDistance() * direction)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListState.pageScrollDistance(): Float {
|
||||
val layoutInfo = layoutInfo
|
||||
val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||
return viewportHeight.toFloat().coerceAtLeast(0f)
|
||||
}
|
||||
|
||||
private fun LazyListState.pageScrollTarget(
|
||||
direction: Int,
|
||||
textLineMetricsByItem: Map<Int, TextLineMetrics>,
|
||||
): PageScrollTarget? {
|
||||
val layoutInfo = layoutInfo
|
||||
val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||
val targetViewportOffset = if (direction > 0) {
|
||||
layoutInfo.viewportEndOffset
|
||||
} else {
|
||||
layoutInfo.viewportStartOffset - viewportHeight
|
||||
}
|
||||
val targetItem = layoutInfo.visibleItemsInfo.firstOrNull { item ->
|
||||
targetViewportOffset >= item.offset && targetViewportOffset < item.offset + item.size
|
||||
} ?: return null
|
||||
val metrics = textLineMetricsByItem[targetItem.index] ?: return null
|
||||
val targetItemOffset = targetViewportOffset - targetItem.offset
|
||||
val lineTop = metrics.lineTopContaining(targetItemOffset) ?: return null
|
||||
|
||||
return PageScrollTarget(targetItem.index, lineTop)
|
||||
}
|
||||
|
||||
private data class PageScrollTarget(
|
||||
val itemIndex: Int,
|
||||
val scrollOffset: Int,
|
||||
)
|
||||
|
||||
private data class TextLineMetrics(
|
||||
val lineTops: List<Int>,
|
||||
val lineBottoms: List<Int>,
|
||||
) {
|
||||
fun lineTopContaining(offsetPx: Int): Int? {
|
||||
val lineIndex = lineTops.indices.firstOrNull { index ->
|
||||
offsetPx >= lineTops[index] && offsetPx < lineBottoms[index]
|
||||
} ?: return null
|
||||
return lineTops[lineIndex]
|
||||
}
|
||||
}
|
||||
|
||||
private fun TextLayoutResult.toTextLineMetrics(): TextLineMetrics =
|
||||
TextLineMetrics(
|
||||
lineTops = List(lineCount) { line -> getLineTop(line).roundToInt() },
|
||||
lineBottoms = List(lineCount) { line -> getLineBottom(line).roundToInt() },
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun DetailsPane(
|
||||
book: Fb2Book,
|
||||
@ -388,6 +469,7 @@ private fun ReaderText(
|
||||
textAlign: TextAlign,
|
||||
modifier: Modifier = Modifier,
|
||||
highlightedRange: ReaderSentenceRange? = null,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
) {
|
||||
val highlightColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val annotatedText = text.toAnnotatedString(language, hyphenation, highlightedRange, highlightColor)
|
||||
@ -433,7 +515,10 @@ private fun ReaderText(
|
||||
style = style,
|
||||
textAlign = textAlign,
|
||||
modifier = textModifier,
|
||||
onTextLayout = { textLayout = it },
|
||||
onTextLayout = {
|
||||
textLayout = it
|
||||
onTextLayout(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user