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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@ -77,6 +78,7 @@ import net.sergeych.toread.text.SoftHyphen
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinuousBookReader(
|
internal fun ContinuousBookReader(
|
||||||
@ -90,6 +92,7 @@ internal fun ContinuousBookReader(
|
|||||||
) {
|
) {
|
||||||
val hyphenation = remember { HyphenationRegistry() }
|
val hyphenation = remember { HyphenationRegistry() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val textLineMetricsByItem = remember(contentPlan) { mutableStateMapOf<Int, TextLineMetrics>() }
|
||||||
val contentPadding = if (isAndroidPlatform()) {
|
val contentPadding = if (isAndroidPlatform()) {
|
||||||
PaddingValues(start = 0.dp, top = 6.dp, end = 0.dp, bottom = 6.dp)
|
PaddingValues(start = 0.dp, top = 6.dp, end = 0.dp, bottom = 6.dp)
|
||||||
} else {
|
} else {
|
||||||
@ -103,12 +106,12 @@ internal fun ContinuousBookReader(
|
|||||||
.pageTurnOnTouchTap(
|
.pageTurnOnTouchTap(
|
||||||
onPageDown = {
|
onPageDown = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState.animateScrollBy(listState.pageScrollDistance())
|
listState.pageScrollByPage(1, textLineMetricsByItem)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPageUp = {
|
onPageUp = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState.animateScrollBy(-listState.pageScrollDistance())
|
listState.pageScrollByPage(-1, textLineMetricsByItem)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -175,6 +178,7 @@ internal fun ContinuousBookReader(
|
|||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
// so we add 6.dp to make it look symmetric
|
// so we add 6.dp to make it look symmetric
|
||||||
modifier = Modifier.padding(start = (element.depth * 8).dp + 6.dp, end = 0.dp),
|
modifier = Modifier.padding(start = (element.depth * 8).dp + 6.dp, end = 0.dp),
|
||||||
|
onTextLayout = { textLineMetricsByItem[itemIndex] = it.toTextLineMetrics() },
|
||||||
)
|
)
|
||||||
is ReaderElement.Subtitle -> ReaderText(
|
is ReaderElement.Subtitle -> ReaderText(
|
||||||
text = element.text,
|
text = element.text,
|
||||||
@ -184,6 +188,7 @@ internal fun ContinuousBookReader(
|
|||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
|
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,
|
onPageUp: () -> Unit,
|
||||||
): Modifier = pointerInput(onPageDown, onPageUp) {
|
): Modifier = pointerInput(onPageDown, onPageUp) {
|
||||||
awaitEachGesture {
|
awaitEachGesture {
|
||||||
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Final)
|
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||||
if (down.type != PointerType.Touch) {
|
if (down.type != PointerType.Touch) {
|
||||||
waitForUpOrCancellation(pass = PointerEventPass.Final)
|
waitForUpOrCancellation(pass = PointerEventPass.Final)
|
||||||
return@awaitEachGesture
|
return@awaitEachGesture
|
||||||
}
|
}
|
||||||
|
|
||||||
val up = waitForUpOrCancellation(pass = PointerEventPass.Final) ?: return@awaitEachGesture
|
val touchSlop = viewConfiguration.touchSlop
|
||||||
if (up.isConsumed) return@awaitEachGesture
|
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) {
|
if (down.position.x < size.width / 2f) {
|
||||||
onPageDown()
|
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 {
|
private fun LazyListState.pageScrollDistance(): Float {
|
||||||
val layoutInfo = layoutInfo
|
val layoutInfo = layoutInfo
|
||||||
val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||||
return viewportHeight.toFloat().coerceAtLeast(0f)
|
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
|
@Composable
|
||||||
private fun DetailsPane(
|
private fun DetailsPane(
|
||||||
book: Fb2Book,
|
book: Fb2Book,
|
||||||
@ -388,6 +469,7 @@ private fun ReaderText(
|
|||||||
textAlign: TextAlign,
|
textAlign: TextAlign,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
highlightedRange: ReaderSentenceRange? = null,
|
highlightedRange: ReaderSentenceRange? = null,
|
||||||
|
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val highlightColor = MaterialTheme.colorScheme.secondaryContainer
|
val highlightColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
val annotatedText = text.toAnnotatedString(language, hyphenation, highlightedRange, highlightColor)
|
val annotatedText = text.toAnnotatedString(language, hyphenation, highlightedRange, highlightColor)
|
||||||
@ -433,7 +515,10 @@ private fun ReaderText(
|
|||||||
style = style,
|
style = style,
|
||||||
textAlign = textAlign,
|
textAlign = textAlign,
|
||||||
modifier = textModifier,
|
modifier = textModifier,
|
||||||
onTextLayout = { textLayout = it },
|
onTextLayout = {
|
||||||
|
textLayout = it
|
||||||
|
onTextLayout(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user